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(
|
return parameters.reduce(
|
||||||
(acc, param) => {
|
(acc, param) => {
|
||||||
const [key, value] = param.split('=');
|
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;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, string>,
|
{} as Record<string, string>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseContentType(contentType?: string): IContentType | null {
|
export function parseContentType(contentType?: string): IContentType | null {
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -695,22 +699,7 @@ function parseContentType(contentType?: string): IContentType | null {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFileName(filename?: string): string | undefined {
|
export function parseContentDisposition(contentDisposition?: string): IContentDisposition | null {
|
||||||
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 {
|
|
||||||
if (!contentDisposition) {
|
if (!contentDisposition) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -725,11 +714,15 @@ function parseContentDisposition(contentDisposition?: string): IContentDispositi
|
||||||
|
|
||||||
const parsedParameters = parseHeaderParameters(parameters);
|
const parsedParameters = parseHeaderParameters(parameters);
|
||||||
|
|
||||||
return {
|
let { filename } = parsedParameters;
|
||||||
type,
|
const wildcard = parsedParameters['filename*'];
|
||||||
filename:
|
if (wildcard) {
|
||||||
parseFileNameStar(parsedParameters['filename*']) ?? parseFileName(parsedParameters.filename),
|
// https://datatracker.ietf.org/doc/html/rfc5987
|
||||||
};
|
const [_encoding, _locale, content] = wildcard?.split("'") ?? [];
|
||||||
|
filename = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type, filename };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIncomingMessage(message: IncomingMessage) {
|
export function parseIncomingMessage(message: IncomingMessage) {
|
||||||
|
|
|
@ -27,6 +27,8 @@ import {
|
||||||
copyInputItems,
|
copyInputItems,
|
||||||
getBinaryDataBuffer,
|
getBinaryDataBuffer,
|
||||||
isFilePathBlocked,
|
isFilePathBlocked,
|
||||||
|
parseContentDisposition,
|
||||||
|
parseContentType,
|
||||||
parseIncomingMessage,
|
parseIncomingMessage,
|
||||||
parseRequestObject,
|
parseRequestObject,
|
||||||
proxyRequestToAxios,
|
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', () => {
|
describe('parseIncomingMessage', () => {
|
||||||
it('parses valid content-type header', () => {
|
it('parses valid content-type header', () => {
|
||||||
const message = mock<IncomingMessage>({
|
const message = mock<IncomingMessage>({
|
||||||
|
@ -170,6 +318,20 @@ describe('NodeExecuteFunctions', () => {
|
||||||
parseIncomingMessage(message);
|
parseIncomingMessage(message);
|
||||||
|
|
||||||
expect(message.contentType).toEqual('application/json');
|
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*', () => {
|
it('parses valid content-disposition header with filename*', () => {
|
||||||
|
|
Loading…
Reference in a new issue