Skip to content

Commit

Permalink
fix(core): Improve header parameter parsing on http client responses (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy authored Nov 28, 2024
1 parent 439a1cc commit 41e9e39
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 23 deletions.
39 changes: 16 additions & 23 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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) {
Expand Down
162 changes: 162 additions & 0 deletions packages/core/test/NodeExecuteFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
copyInputItems,
getBinaryDataBuffer,
isFilePathBlocked,
parseContentDisposition,
parseContentType,
parseIncomingMessage,
parseRequestObject,
proxyRequestToAxios,
Expand Down Expand Up @@ -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>({
Expand All @@ -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*', () => {
Expand Down

0 comments on commit 41e9e39

Please sign in to comment.