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 { 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;

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')
.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')

View File

@@ -92,6 +92,23 @@
}
},
"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": {
@@ -184,6 +201,21 @@
},
"json": {}
}
],
"HTTP Request (v4)2": [
{
"binary": {
"data": {
"data": "dGVzdA==",
"mimeType": "text/plain",
"fileType": "text",
"fileExtension": "txt",
"fileName": "text.txt",
"fileSize": "4 B"
}
},
"json": {}
}
]
},
"connections": {
@@ -219,6 +251,11 @@
"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');
});
});