mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
fix(HTTP Request Node): Process text files (#16226)
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
22
packages/nodes-base/nodes/HttpRequest/V3/utils/binaryData.ts
Normal file
22
packages/nodes-base/nodes/HttpRequest/V3/utils/binaryData.ts
Normal 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;
|
||||||
|
};
|
||||||
9
packages/nodes-base/nodes/HttpRequest/V3/utils/parse.ts
Normal file
9
packages/nodes-base/nodes/HttpRequest/V3/utils/parse.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const mimeTypeFromResponse = (
|
||||||
|
responseContentType: string | undefined,
|
||||||
|
): string | undefined => {
|
||||||
|
if (!responseContentType) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseContentType.split(' ')[0].split(';')[0];
|
||||||
|
};
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -92,6 +92,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"position": [1020, 720]
|
"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": {
|
"pinData": {
|
||||||
@@ -184,6 +201,21 @@
|
|||||||
},
|
},
|
||||||
"json": {}
|
"json": {}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"HTTP Request (v4)2": [
|
||||||
|
{
|
||||||
|
"binary": {
|
||||||
|
"data": {
|
||||||
|
"data": "dGVzdA==",
|
||||||
|
"mimeType": "text/plain",
|
||||||
|
"fileType": "text",
|
||||||
|
"fileExtension": "txt",
|
||||||
|
"fileName": "text.txt",
|
||||||
|
"fileSize": "4 B"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json": {}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
@@ -219,6 +251,11 @@
|
|||||||
"node": "Content Disposition",
|
"node": "Content Disposition",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node": "HTTP Request (v4)2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user