fix(HTTP Request Node): Process text files (#16226)

This commit is contained in:
Dana
2025-06-13 16:05:15 +02:00
committed by GitHub
parent 88e3c90e71
commit 0d5ac1f822
7 changed files with 378 additions and 235 deletions

View File

@@ -27,6 +27,8 @@ import type { Readable } from 'stream';
import { keysToLowercase } from '@utils/utilities'; import { keysToLowercase } from '@utils/utilities';
import { mainProperties } from './Description'; import { mainProperties } from './Description';
import { setFilename } from './utils/binaryData';
import { mimeTypeFromResponse } from './utils/parse';
import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions'; import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions';
import { import {
binaryContentTypes, binaryContentTypes,
@@ -123,6 +125,8 @@ export class HttpRequestV3 implements INodeType {
let autoDetectResponseFormat = false; let autoDetectResponseFormat = false;
let responseFileName: string | undefined;
// Can not be defined on a per item level // Can not be defined on a per item level
const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, { const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, {
rawExpressions: true, rawExpressions: true,
@@ -240,12 +244,19 @@ export class HttpRequestV3 implements INodeType {
allowUnauthorizedCerts: boolean; allowUnauthorizedCerts: boolean;
queryParameterArrays: 'indices' | 'brackets' | 'repeat'; queryParameterArrays: 'indices' | 'brackets' | 'repeat';
response: { response: {
response: { neverError: boolean; responseFormat: string; fullResponse: boolean }; response: {
neverError: boolean;
responseFormat: string;
fullResponse: boolean;
outputPropertyName: string;
};
}; };
redirect: { redirect: { maxRedirects: number; followRedirects: boolean } }; redirect: { redirect: { maxRedirects: number; followRedirects: boolean } };
lowercaseHeaders: boolean; lowercaseHeaders: boolean;
}; };
responseFileName = response?.response?.outputPropertyName;
const url = this.getNodeParameter('url', itemIndex) as string; const url = this.getNodeParameter('url', itemIndex) as string;
const responseFormat = response?.response?.responseFormat || 'autodetect'; const responseFormat = response?.response?.responseFormat || 'autodetect';
@@ -904,17 +915,14 @@ export class HttpRequestV3 implements INodeType {
const preparedBinaryData = await this.helpers.prepareBinaryData( const preparedBinaryData = await this.helpers.prepareBinaryData(
binaryData, binaryData,
undefined, undefined,
responseContentType || undefined, mimeTypeFromResponse(responseContentType),
); );
if ( preparedBinaryData.fileName = setFilename(
!preparedBinaryData.fileName && preparedBinaryData,
preparedBinaryData.fileExtension && requestOptions,
typeof requestOptions.uri === 'string' && responseFileName,
requestOptions.uri.endsWith(preparedBinaryData.fileExtension) );
) {
preparedBinaryData.fileName = requestOptions.uri.split('/').pop();
}
newItem.binary![outputPropertyName] = preparedBinaryData; newItem.binary![outputPropertyName] = preparedBinaryData;

View File

@@ -0,0 +1,22 @@
import type { IBinaryData, IRequestOptions } from 'n8n-workflow';
export const setFilename = (
preparedBinaryData: IBinaryData,
requestOptions: IRequestOptions,
responseFileName: string | undefined,
) => {
if (
!preparedBinaryData.fileName &&
preparedBinaryData.fileExtension &&
typeof requestOptions.uri === 'string' &&
requestOptions.uri.endsWith(preparedBinaryData.fileExtension)
) {
return requestOptions.uri.split('/').pop();
}
if (!preparedBinaryData.fileName && preparedBinaryData.fileExtension) {
return `${responseFileName ?? 'data'}.${preparedBinaryData.fileExtension}`;
}
return preparedBinaryData.fileName;
};

View File

@@ -0,0 +1,9 @@
export const mimeTypeFromResponse = (
responseContentType: string | undefined,
): string | undefined => {
if (!responseContentType) {
return undefined;
}
return responseContentType.split(' ')[0].split(';')[0];
};

View File

@@ -10,6 +10,11 @@ describe('Test Binary Data Download', () => {
.get('/path/to/image.png') .get('/path/to/image.png')
.reply(200, Buffer.from('test'), { 'content-type': 'image/png' }); .reply(200, Buffer.from('test'), { 'content-type': 'image/png' });
nock(baseUrl)
.persist()
.get('/path/to/text.txt')
.reply(200, Buffer.from('test'), { 'content-type': 'text/plain; charset=utf-8' });
nock(baseUrl) nock(baseUrl)
.persist() .persist()
.get('/redirect-to-image') .get('/redirect-to-image')

View File

@@ -1,227 +1,264 @@
{ {
"name": "Download as Binary Data", "name": "Download as Binary Data",
"nodes": [ "nodes": [
{ {
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"parameters": {}, "parameters": {},
"position": [580, 300] "position": [580, 300]
}, },
{ {
"name": "HTTP Request (v1)", "name": "HTTP Request (v1)",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 1, "typeVersion": 1,
"parameters": { "parameters": {
"url": "https://dummy.domain/path/to/image.png", "url": "https://dummy.domain/path/to/image.png",
"responseFormat": "file" "responseFormat": "file"
}, },
"position": [1020, -100] "position": [1020, -100]
}, },
{ {
"name": "HTTP Request (v2)", "name": "HTTP Request (v2)",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 2, "typeVersion": 2,
"parameters": { "parameters": {
"url": "https://dummy.domain/path/to/image.png", "url": "https://dummy.domain/path/to/image.png",
"responseFormat": "file", "responseFormat": "file",
"options": {} "options": {}
}, },
"position": [1020, 80] "position": [1020, 80]
}, },
{ {
"name": "HTTP Request (v3)", "name": "HTTP Request (v3)",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 3, "typeVersion": 3,
"parameters": { "parameters": {
"url": "https://dummy.domain/path/to/image.png", "url": "https://dummy.domain/path/to/image.png",
"options": { "options": {
"response": { "response": {
"response": { "response": {
"responseFormat": "file" "responseFormat": "file"
} }
} }
} }
}, },
"position": [1020, 240] "position": [1020, 240]
}, },
{ {
"name": "HTTP Request (v4)", "name": "HTTP Request (v4)",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4, "typeVersion": 4,
"parameters": { "parameters": {
"url": "https://dummy.domain/path/to/image.png", "url": "https://dummy.domain/path/to/image.png",
"options": { "options": {
"response": { "response": {
"response": { "response": {
"responseFormat": "file" "responseFormat": "file"
} }
} }
} }
}, },
"position": [1020, 400] "position": [1020, 400]
}, },
{ {
"name": "Follow Redirect", "name": "Follow Redirect",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1, "typeVersion": 4.1,
"parameters": { "parameters": {
"url": "https://dummy.domain/redirect-to-image", "url": "https://dummy.domain/redirect-to-image",
"options": { "options": {
"response": { "response": {
"response": { "response": {
"responseFormat": "file" "responseFormat": "file"
} }
} }
} }
}, },
"position": [1020, 560] "position": [1020, 560]
}, },
{ {
"name": "Content Disposition", "name": "Content Disposition",
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1, "typeVersion": 4.1,
"parameters": { "parameters": {
"url": "https://dummy.domain/custom-content-disposition", "url": "https://dummy.domain/custom-content-disposition",
"options": { "options": {
"response": { "response": {
"response": { "response": {
"responseFormat": "file" "responseFormat": "file"
} }
} }
} }
}, },
"position": [1020, 720] "position": [1020, 720]
} },
], {
"pinData": { "parameters": {
"HTTP Request (v1)": [ "url": "https://dummy.domain/path/to/text.txt",
{ "options": {
"binary": { "response": {
"data": { "response": {
"data": "dGVzdA==", "responseFormat": "file"
"mimeType": "image/png", }
"fileType": "image", }
"fileExtension": "png", }
"fileName": "image.png", },
"fileSize": "4 B" "name": "HTTP Request (v4)2",
} "type": "n8n-nodes-base.httpRequest",
}, "typeVersion": 4,
"json": {} "position": [1680, 600],
} "id": "665fef5c-6380-4bc0-be2a-430446b140ca"
], }
"HTTP Request (v2)": [ ],
{ "pinData": {
"binary": { "HTTP Request (v1)": [
"data": { {
"data": "dGVzdA==", "binary": {
"mimeType": "image/png", "data": {
"fileType": "image", "data": "dGVzdA==",
"fileExtension": "png", "mimeType": "image/png",
"fileName": "image.png", "fileType": "image",
"fileSize": "4 B" "fileExtension": "png",
} "fileName": "image.png",
}, "fileSize": "4 B"
"json": {} }
} },
], "json": {}
"HTTP Request (v3)": [ }
{ ],
"binary": { "HTTP Request (v2)": [
"data": { {
"data": "dGVzdA==", "binary": {
"mimeType": "image/png", "data": {
"fileType": "image", "data": "dGVzdA==",
"fileExtension": "png", "mimeType": "image/png",
"fileName": "image.png", "fileType": "image",
"fileSize": "4 B" "fileExtension": "png",
} "fileName": "image.png",
}, "fileSize": "4 B"
"json": {} }
} },
], "json": {}
"HTTP Request (v4)": [ }
{ ],
"binary": { "HTTP Request (v3)": [
"data": { {
"data": "dGVzdA==", "binary": {
"mimeType": "image/png", "data": {
"fileType": "image", "data": "dGVzdA==",
"fileExtension": "png", "mimeType": "image/png",
"fileName": "image.png", "fileType": "image",
"fileSize": "4 B" "fileExtension": "png",
} "fileName": "image.png",
}, "fileSize": "4 B"
"json": {} }
} },
], "json": {}
"Follow Redirect": [ }
{ ],
"binary": { "HTTP Request (v4)": [
"data": { {
"data": "dGVzdA==", "binary": {
"mimeType": "image/png", "data": {
"fileType": "image", "data": "dGVzdA==",
"fileExtension": "png", "mimeType": "image/png",
"fileName": "image.png", "fileType": "image",
"fileSize": "4 B" "fileExtension": "png",
} "fileName": "image.png",
}, "fileSize": "4 B"
"json": {} }
} },
], "json": {}
"Content Disposition": [ }
{ ],
"binary": { "Follow Redirect": [
"data": { {
"data": "dGVzdGluZw==", "binary": {
"mimeType": "image/jpeg", "data": {
"fileType": "image", "data": "dGVzdA==",
"fileExtension": "jpg", "mimeType": "image/png",
"fileName": "testing.jpg", "fileType": "image",
"fileSize": "7 B" "fileExtension": "png",
} "fileName": "image.png",
}, "fileSize": "4 B"
"json": {} }
} },
] "json": {}
}, }
"connections": { ],
"When clicking \"Execute Workflow\"": { "Content Disposition": [
"main": [ {
[ "binary": {
{ "data": {
"node": "HTTP Request (v1)", "data": "dGVzdGluZw==",
"type": "main", "mimeType": "image/jpeg",
"index": 0 "fileType": "image",
}, "fileExtension": "jpg",
{ "fileName": "testing.jpg",
"node": "HTTP Request (v2)", "fileSize": "7 B"
"type": "main", }
"index": 0 },
}, "json": {}
{ }
"node": "HTTP Request (v3)", ],
"type": "main", "HTTP Request (v4)2": [
"index": 0 {
}, "binary": {
{ "data": {
"node": "HTTP Request (v4)", "data": "dGVzdA==",
"type": "main", "mimeType": "text/plain",
"index": 0 "fileType": "text",
}, "fileExtension": "txt",
{ "fileName": "text.txt",
"node": "Follow Redirect", "fileSize": "4 B"
"type": "main", }
"index": 0 },
}, "json": {}
{ }
"node": "Content Disposition", ]
"type": "main", },
"index": 0 "connections": {
} "When clicking \"Execute Workflow\"": {
] "main": [
] [
} {
} "node": "HTTP Request (v1)",
"type": "main",
"index": 0
},
{
"node": "HTTP Request (v2)",
"type": "main",
"index": 0
},
{
"node": "HTTP Request (v3)",
"type": "main",
"index": 0
},
{
"node": "HTTP Request (v4)",
"type": "main",
"index": 0
},
{
"node": "Follow Redirect",
"type": "main",
"index": 0
},
{
"node": "Content Disposition",
"type": "main",
"index": 0
},
{
"node": "HTTP Request (v4)2",
"type": "main",
"index": 0
}
]
]
}
}
} }

View File

@@ -0,0 +1,29 @@
import type { IBinaryData, IRequestOptions } from 'n8n-workflow';
import { setFilename } from '../../V3/utils/binaryData';
describe('setFilename', () => {
it('returns filename from URI if fileName is missing and URI ends with fileExtension', () => {
const preparedBinaryData = { fileExtension: 'png' } as IBinaryData;
const requestOptions = { uri: 'https://example.com/image.png' } as IRequestOptions;
expect(setFilename(preparedBinaryData, requestOptions, undefined)).toBe('image.png');
});
it('returns constructed filename if fileName is missing and URI does not end with fileExtension', () => {
const preparedBinaryData = { fileExtension: 'jpg' } as IBinaryData;
const requestOptions = { uri: 'https://example.com/image.png' } as IRequestOptions;
expect(setFilename(preparedBinaryData, requestOptions, 'response')).toBe('response.jpg');
});
it('returns constructed filename with default "data" if responseFileName is undefined', () => {
const preparedBinaryData = { fileExtension: 'txt' } as IBinaryData;
const requestOptions = { uri: 'https://example.com/file' } as IRequestOptions;
expect(setFilename(preparedBinaryData, requestOptions, undefined)).toBe('data.txt');
});
it('returns fileName if it exists', () => {
const preparedBinaryData = { fileName: 'myfile.pdf', fileExtension: 'pdf' } as IBinaryData;
const requestOptions = { uri: 'https://example.com/file.pdf' } as IRequestOptions;
expect(setFilename(preparedBinaryData, requestOptions, 'response')).toBe('myfile.pdf');
});
});

View File

@@ -0,0 +1,33 @@
import { mimeTypeFromResponse } from '../../V3/utils/parse';
describe('mimeTypeFromResponse', () => {
it('should return undefined if input is undefined', () => {
expect(mimeTypeFromResponse(undefined)).toBeUndefined();
});
it('should return the mime type for a simple type', () => {
expect(mimeTypeFromResponse('image/png')).toBe('image/png');
});
it('should strip charset from content type', () => {
expect(mimeTypeFromResponse('text/html; charset=utf-8')).toBe('text/html');
});
it('should strip charset from content type', () => {
expect(mimeTypeFromResponse('text/plain; charset=utf-8')).toBe('text/plain');
});
it('should strip boundary from multipart content type', () => {
expect(mimeTypeFromResponse('multipart/form-data; boundary=ExampleBoundaryString')).toBe(
'multipart/form-data',
);
});
it('should handle content type with extra spaces', () => {
expect(mimeTypeFromResponse('application/json ; charset=utf-8')).toBe('application/json');
});
it('should handle content type with space before semicolon', () => {
expect(mimeTypeFromResponse('application/xml ;charset=utf-8')).toBe('application/xml');
});
});