mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(core): Improve header parameter parsing on http client responses (#11953)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
439a1cc4f3
commit
41e9e39b5b
|
@ -675,14 +675,18 @@ function parseHeaderParameters(parameters: string[]): Record<string, string> {
|
|||
return parameters.reduce(
|
||||
(acc, param) => {
|
||||
const [key, value] = param.split('=');
|
||||
acc[key.toLowerCase().trim()] = decodeURIComponent(value);
|
||||
let decodedValue = decodeURIComponent(value).trim();
|
||||
if (decodedValue.startsWith('"') && decodedValue.endsWith('"')) {
|
||||
decodedValue = decodedValue.slice(1, -1);
|
||||
}
|
||||
acc[key.toLowerCase().trim()] = decodedValue;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
function parseContentType(contentType?: string): IContentType | null {
|
||||
export function parseContentType(contentType?: string): IContentType | null {
|
||||
if (!contentType) {
|
||||
return null;
|
||||
}
|
||||
|
@ -695,22 +699,7 @@ function parseContentType(contentType?: string): IContentType | null {
|
|||
};
|
||||
}
|
||||
|
||||
function parseFileName(filename?: string): string | undefined {
|
||||
if (filename?.startsWith('"') && filename?.endsWith('"')) {
|
||||
return filename.slice(1, -1);
|
||||
}
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc5987
|
||||
function parseFileNameStar(filename?: string): string | undefined {
|
||||
const [_encoding, _locale, content] = parseFileName(filename)?.split("'") ?? [];
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function parseContentDisposition(contentDisposition?: string): IContentDisposition | null {
|
||||
export function parseContentDisposition(contentDisposition?: string): IContentDisposition | null {
|
||||
if (!contentDisposition) {
|
||||
return null;
|
||||
}
|
||||
|
@ -725,11 +714,15 @@ function parseContentDisposition(contentDisposition?: string): IContentDispositi
|
|||
|
||||
const parsedParameters = parseHeaderParameters(parameters);
|
||||
|
||||
return {
|
||||
type,
|
||||
filename:
|
||||
parseFileNameStar(parsedParameters['filename*']) ?? parseFileName(parsedParameters.filename),
|
||||
};
|
||||
let { filename } = parsedParameters;
|
||||
const wildcard = parsedParameters['filename*'];
|
||||
if (wildcard) {
|
||||
// https://datatracker.ietf.org/doc/html/rfc5987
|
||||
const [_encoding, _locale, content] = wildcard?.split("'") ?? [];
|
||||
filename = content;
|
||||
}
|
||||
|
||||
return { type, filename };
|
||||
}
|
||||
|
||||
export function parseIncomingMessage(message: IncomingMessage) {
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
copyInputItems,
|
||||
getBinaryDataBuffer,
|
||||
isFilePathBlocked,
|
||||
parseContentDisposition,
|
||||
parseContentType,
|
||||
parseIncomingMessage,
|
||||
parseRequestObject,
|
||||
proxyRequestToAxios,
|
||||
|
@ -150,6 +152,152 @@ describe('NodeExecuteFunctions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parseContentType', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: 'text/plain',
|
||||
expected: {
|
||||
type: 'text/plain',
|
||||
parameters: {
|
||||
charset: 'utf-8',
|
||||
},
|
||||
},
|
||||
description: 'should parse basic content type',
|
||||
},
|
||||
{
|
||||
input: 'TEXT/PLAIN',
|
||||
expected: {
|
||||
type: 'text/plain',
|
||||
parameters: {
|
||||
charset: 'utf-8',
|
||||
},
|
||||
},
|
||||
description: 'should convert type to lowercase',
|
||||
},
|
||||
{
|
||||
input: 'text/html; charset=iso-8859-1',
|
||||
expected: {
|
||||
type: 'text/html',
|
||||
parameters: {
|
||||
charset: 'iso-8859-1',
|
||||
},
|
||||
},
|
||||
description: 'should parse content type with charset',
|
||||
},
|
||||
{
|
||||
input: 'application/json; charset=utf-8; boundary=---123',
|
||||
expected: {
|
||||
type: 'application/json',
|
||||
parameters: {
|
||||
charset: 'utf-8',
|
||||
boundary: '---123',
|
||||
},
|
||||
},
|
||||
description: 'should parse content type with multiple parameters',
|
||||
},
|
||||
{
|
||||
input: 'text/plain; charset="utf-8"; filename="test.txt"',
|
||||
expected: {
|
||||
type: 'text/plain',
|
||||
parameters: {
|
||||
charset: 'utf-8',
|
||||
filename: 'test.txt',
|
||||
},
|
||||
},
|
||||
description: 'should handle quoted parameter values',
|
||||
},
|
||||
{
|
||||
input: 'text/plain; filename=%22test%20file.txt%22',
|
||||
expected: {
|
||||
type: 'text/plain',
|
||||
parameters: {
|
||||
charset: 'utf-8',
|
||||
filename: 'test file.txt',
|
||||
},
|
||||
},
|
||||
description: 'should handle encoded parameter values',
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: null,
|
||||
description: 'should return null for undefined input',
|
||||
},
|
||||
{
|
||||
input: '',
|
||||
expected: null,
|
||||
description: 'should return null for empty string',
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$description', ({ input, expected }) => {
|
||||
expect(parseContentType(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseContentDisposition', () => {
|
||||
const testCases = [
|
||||
{
|
||||
input: 'attachment; filename="file.txt"',
|
||||
expected: { type: 'attachment', filename: 'file.txt' },
|
||||
description: 'should parse basic content disposition',
|
||||
},
|
||||
{
|
||||
input: 'attachment; filename=file.txt',
|
||||
expected: { type: 'attachment', filename: 'file.txt' },
|
||||
description: 'should parse filename without quotes',
|
||||
},
|
||||
{
|
||||
input: 'inline; filename="image.jpg"',
|
||||
expected: { type: 'inline', filename: 'image.jpg' },
|
||||
description: 'should parse inline disposition',
|
||||
},
|
||||
{
|
||||
input: 'attachment; filename="my file.pdf"',
|
||||
expected: { type: 'attachment', filename: 'my file.pdf' },
|
||||
description: 'should parse filename with spaces',
|
||||
},
|
||||
{
|
||||
input: "attachment; filename*=UTF-8''my%20file.txt",
|
||||
expected: { type: 'attachment', filename: 'my file.txt' },
|
||||
description: 'should parse filename* parameter (RFC 5987)',
|
||||
},
|
||||
{
|
||||
input: 'filename="test.txt"',
|
||||
expected: { type: 'attachment', filename: 'test.txt' },
|
||||
description: 'should handle invalid syntax but with filename',
|
||||
},
|
||||
{
|
||||
input: 'filename=test.txt',
|
||||
expected: { type: 'attachment', filename: 'test.txt' },
|
||||
description: 'should handle invalid syntax with only filename parameter',
|
||||
},
|
||||
{
|
||||
input: undefined,
|
||||
expected: null,
|
||||
description: 'should return null for undefined input',
|
||||
},
|
||||
{
|
||||
input: '',
|
||||
expected: null,
|
||||
description: 'should return null for empty string',
|
||||
},
|
||||
{
|
||||
input: 'attachment; filename="%F0%9F%98%80.txt"',
|
||||
expected: { type: 'attachment', filename: '😀.txt' },
|
||||
description: 'should handle encoded filenames',
|
||||
},
|
||||
{
|
||||
input: 'attachment; size=123; filename="test.txt"; creation-date="Thu, 1 Jan 2020"',
|
||||
expected: { type: 'attachment', filename: 'test.txt' },
|
||||
description: 'should handle multiple parameters',
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)('$description', ({ input, expected }) => {
|
||||
expect(parseContentDisposition(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIncomingMessage', () => {
|
||||
it('parses valid content-type header', () => {
|
||||
const message = mock<IncomingMessage>({
|
||||
|
@ -170,6 +318,20 @@ describe('NodeExecuteFunctions', () => {
|
|||
parseIncomingMessage(message);
|
||||
|
||||
expect(message.contentType).toEqual('application/json');
|
||||
expect(message.encoding).toEqual('utf-8');
|
||||
});
|
||||
|
||||
it('parses valid content-type header with encoding wrapped in quotes', () => {
|
||||
const message = mock<IncomingMessage>({
|
||||
headers: {
|
||||
'content-type': 'application/json; charset="utf-8"',
|
||||
'content-disposition': undefined,
|
||||
},
|
||||
});
|
||||
parseIncomingMessage(message);
|
||||
|
||||
expect(message.contentType).toEqual('application/json');
|
||||
expect(message.encoding).toEqual('utf-8');
|
||||
});
|
||||
|
||||
it('parses valid content-disposition header with filename*', () => {
|
||||
|
|
Loading…
Reference in a new issue