diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index 9448c4add0..785b1e8de5 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -27,6 +27,8 @@ import type { Readable } from 'stream'; import { keysToLowercase } from '@utils/utilities'; import { mainProperties } from './Description'; +import { setFilename } from './utils/binaryData'; +import { mimeTypeFromResponse } from './utils/parse'; import type { BodyParameter, IAuthDataSanitizeKeys } from '../GenericFunctions'; import { binaryContentTypes, @@ -123,6 +125,8 @@ export class HttpRequestV3 implements INodeType { let autoDetectResponseFormat = false; + let responseFileName: string | undefined; + // Can not be defined on a per item level const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, { rawExpressions: true, @@ -240,12 +244,19 @@ export class HttpRequestV3 implements INodeType { allowUnauthorizedCerts: boolean; queryParameterArrays: 'indices' | 'brackets' | 'repeat'; response: { - response: { neverError: boolean; responseFormat: string; fullResponse: boolean }; + response: { + neverError: boolean; + responseFormat: string; + fullResponse: boolean; + outputPropertyName: string; + }; }; redirect: { redirect: { maxRedirects: number; followRedirects: boolean } }; lowercaseHeaders: boolean; }; + responseFileName = response?.response?.outputPropertyName; + const url = this.getNodeParameter('url', itemIndex) as string; const responseFormat = response?.response?.responseFormat || 'autodetect'; @@ -904,17 +915,14 @@ export class HttpRequestV3 implements INodeType { const preparedBinaryData = await this.helpers.prepareBinaryData( binaryData, undefined, - responseContentType || undefined, + mimeTypeFromResponse(responseContentType), ); - if ( - !preparedBinaryData.fileName && - preparedBinaryData.fileExtension && - typeof requestOptions.uri === 'string' && - requestOptions.uri.endsWith(preparedBinaryData.fileExtension) - ) { - preparedBinaryData.fileName = requestOptions.uri.split('/').pop(); - } + preparedBinaryData.fileName = setFilename( + preparedBinaryData, + requestOptions, + responseFileName, + ); newItem.binary![outputPropertyName] = preparedBinaryData; diff --git a/packages/nodes-base/nodes/HttpRequest/V3/utils/binaryData.ts b/packages/nodes-base/nodes/HttpRequest/V3/utils/binaryData.ts new file mode 100644 index 0000000000..ca84605329 --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/V3/utils/binaryData.ts @@ -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; +}; diff --git a/packages/nodes-base/nodes/HttpRequest/V3/utils/parse.ts b/packages/nodes-base/nodes/HttpRequest/V3/utils/parse.ts new file mode 100644 index 0000000000..33d7a9007d --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/V3/utils/parse.ts @@ -0,0 +1,9 @@ +export const mimeTypeFromResponse = ( + responseContentType: string | undefined, +): string | undefined => { + if (!responseContentType) { + return undefined; + } + + return responseContentType.split(' ')[0].split(';')[0]; +}; diff --git a/packages/nodes-base/nodes/HttpRequest/test/binaryData/HttpRequest.test.ts b/packages/nodes-base/nodes/HttpRequest/test/binaryData/HttpRequest.test.ts index 5896beb189..5877411e1d 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/binaryData/HttpRequest.test.ts +++ b/packages/nodes-base/nodes/HttpRequest/test/binaryData/HttpRequest.test.ts @@ -10,6 +10,11 @@ describe('Test Binary Data Download', () => { .get('/path/to/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) .persist() .get('/redirect-to-image') diff --git a/packages/nodes-base/nodes/HttpRequest/test/binaryData/binaryData.test.json b/packages/nodes-base/nodes/HttpRequest/test/binaryData/binaryData.test.json index cf04374320..791fbe19a0 100644 --- a/packages/nodes-base/nodes/HttpRequest/test/binaryData/binaryData.test.json +++ b/packages/nodes-base/nodes/HttpRequest/test/binaryData/binaryData.test.json @@ -1,227 +1,264 @@ { - "name": "Download as Binary Data", - "nodes": [ - { - "name": "When clicking \"Execute Workflow\"", - "type": "n8n-nodes-base.manualTrigger", - "typeVersion": 1, - "parameters": {}, - "position": [580, 300] - }, - { - "name": "HTTP Request (v1)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 1, - "parameters": { - "url": "https://dummy.domain/path/to/image.png", - "responseFormat": "file" - }, - "position": [1020, -100] - }, - { - "name": "HTTP Request (v2)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 2, - "parameters": { - "url": "https://dummy.domain/path/to/image.png", - "responseFormat": "file", - "options": {} - }, - "position": [1020, 80] - }, - { - "name": "HTTP Request (v3)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "parameters": { - "url": "https://dummy.domain/path/to/image.png", - "options": { - "response": { - "response": { - "responseFormat": "file" - } - } - } - }, - "position": [1020, 240] - }, - { - "name": "HTTP Request (v4)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4, - "parameters": { - "url": "https://dummy.domain/path/to/image.png", - "options": { - "response": { - "response": { - "responseFormat": "file" - } - } - } - }, - "position": [1020, 400] - }, - { - "name": "Follow Redirect", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "parameters": { - "url": "https://dummy.domain/redirect-to-image", - "options": { - "response": { - "response": { - "responseFormat": "file" - } - } - } - }, - "position": [1020, 560] - }, - { - "name": "Content Disposition", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "parameters": { - "url": "https://dummy.domain/custom-content-disposition", - "options": { - "response": { - "response": { - "responseFormat": "file" - } - } - } - }, - "position": [1020, 720] - } - ], - "pinData": { - "HTTP Request (v1)": [ - { - "binary": { - "data": { - "data": "dGVzdA==", - "mimeType": "image/png", - "fileType": "image", - "fileExtension": "png", - "fileName": "image.png", - "fileSize": "4 B" - } - }, - "json": {} - } - ], - "HTTP Request (v2)": [ - { - "binary": { - "data": { - "data": "dGVzdA==", - "mimeType": "image/png", - "fileType": "image", - "fileExtension": "png", - "fileName": "image.png", - "fileSize": "4 B" - } - }, - "json": {} - } - ], - "HTTP Request (v3)": [ - { - "binary": { - "data": { - "data": "dGVzdA==", - "mimeType": "image/png", - "fileType": "image", - "fileExtension": "png", - "fileName": "image.png", - "fileSize": "4 B" - } - }, - "json": {} - } - ], - "HTTP Request (v4)": [ - { - "binary": { - "data": { - "data": "dGVzdA==", - "mimeType": "image/png", - "fileType": "image", - "fileExtension": "png", - "fileName": "image.png", - "fileSize": "4 B" - } - }, - "json": {} - } - ], - "Follow Redirect": [ - { - "binary": { - "data": { - "data": "dGVzdA==", - "mimeType": "image/png", - "fileType": "image", - "fileExtension": "png", - "fileName": "image.png", - "fileSize": "4 B" - } - }, - "json": {} - } - ], - "Content Disposition": [ - { - "binary": { - "data": { - "data": "dGVzdGluZw==", - "mimeType": "image/jpeg", - "fileType": "image", - "fileExtension": "jpg", - "fileName": "testing.jpg", - "fileSize": "7 B" - } - }, - "json": {} - } - ] - }, - "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 - } - ] - ] - } - } + "name": "Download as Binary Data", + "nodes": [ + { + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "parameters": {}, + "position": [580, 300] + }, + { + "name": "HTTP Request (v1)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 1, + "parameters": { + "url": "https://dummy.domain/path/to/image.png", + "responseFormat": "file" + }, + "position": [1020, -100] + }, + { + "name": "HTTP Request (v2)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 2, + "parameters": { + "url": "https://dummy.domain/path/to/image.png", + "responseFormat": "file", + "options": {} + }, + "position": [1020, 80] + }, + { + "name": "HTTP Request (v3)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "parameters": { + "url": "https://dummy.domain/path/to/image.png", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "position": [1020, 240] + }, + { + "name": "HTTP Request (v4)", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "parameters": { + "url": "https://dummy.domain/path/to/image.png", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "position": [1020, 400] + }, + { + "name": "Follow Redirect", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "parameters": { + "url": "https://dummy.domain/redirect-to-image", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "position": [1020, 560] + }, + { + "name": "Content Disposition", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "parameters": { + "url": "https://dummy.domain/custom-content-disposition", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "position": [1020, 720] + }, + { + "parameters": { + "url": "https://dummy.domain/path/to/text.txt", + "options": { + "response": { + "response": { + "responseFormat": "file" + } + } + } + }, + "name": "HTTP Request (v4)2", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [1680, 600], + "id": "665fef5c-6380-4bc0-be2a-430446b140ca" + } + ], + "pinData": { + "HTTP Request (v1)": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "image/png", + "fileType": "image", + "fileExtension": "png", + "fileName": "image.png", + "fileSize": "4 B" + } + }, + "json": {} + } + ], + "HTTP Request (v2)": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "image/png", + "fileType": "image", + "fileExtension": "png", + "fileName": "image.png", + "fileSize": "4 B" + } + }, + "json": {} + } + ], + "HTTP Request (v3)": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "image/png", + "fileType": "image", + "fileExtension": "png", + "fileName": "image.png", + "fileSize": "4 B" + } + }, + "json": {} + } + ], + "HTTP Request (v4)": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "image/png", + "fileType": "image", + "fileExtension": "png", + "fileName": "image.png", + "fileSize": "4 B" + } + }, + "json": {} + } + ], + "Follow Redirect": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "image/png", + "fileType": "image", + "fileExtension": "png", + "fileName": "image.png", + "fileSize": "4 B" + } + }, + "json": {} + } + ], + "Content Disposition": [ + { + "binary": { + "data": { + "data": "dGVzdGluZw==", + "mimeType": "image/jpeg", + "fileType": "image", + "fileExtension": "jpg", + "fileName": "testing.jpg", + "fileSize": "7 B" + } + }, + "json": {} + } + ], + "HTTP Request (v4)2": [ + { + "binary": { + "data": { + "data": "dGVzdA==", + "mimeType": "text/plain", + "fileType": "text", + "fileExtension": "txt", + "fileName": "text.txt", + "fileSize": "4 B" + } + }, + "json": {} + } + ] + }, + "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 + } + ] + ] + } + } } diff --git a/packages/nodes-base/nodes/HttpRequest/test/utils/binaryData.test.ts b/packages/nodes-base/nodes/HttpRequest/test/utils/binaryData.test.ts new file mode 100644 index 0000000000..3bb2ef895f --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/test/utils/binaryData.test.ts @@ -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'); + }); +}); diff --git a/packages/nodes-base/nodes/HttpRequest/test/utils/parse.test.ts b/packages/nodes-base/nodes/HttpRequest/test/utils/parse.test.ts new file mode 100644 index 0000000000..f03051d9cd --- /dev/null +++ b/packages/nodes-base/nodes/HttpRequest/test/utils/parse.test.ts @@ -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'); + }); +});