Skip to content

Commit

Permalink
refactor(core)!: removed support for regex on binary validation and c…
Browse files Browse the repository at this point in the history
…ase-sensitive headers

BREAKING CHANGE: now regex is not support anymore due the slow performance and we don't lower case
all the headers, so the content-encoding and content-type must be lowercase
  • Loading branch information
H4ad committed Jun 7, 2023
1 parent 483856e commit 4fb3a39
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 68 deletions.
2 changes: 1 addition & 1 deletion src/@types/binary-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface BinarySettingsContentHeaders {
/**
* The list of content types that will be treated as binary
*/
contentTypes: (string | RegExp)[];
contentTypes: string[];

/**
* The list of content encodings that will be treated as binary
Expand Down
15 changes: 11 additions & 4 deletions src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ export const DEFAULT_BINARY_ENCODINGS: string[] = ['gzip', 'deflate', 'br'];
* Default content types that are treated as binary, they are compared with the `Content-Type` header.
*
* @breadcrumb Core / Constants
* @defaultValue [new RegExp('^image/.*$'), new RegExp('^video/.*$'), 'application/pdf']
* @defaultValue ['image/png', 'image/jpeg', 'image/jpg', 'image/avif', 'image/bmp', 'image/x-png', 'image/gif', 'image/webp', 'video/mp4', 'application/pdf']
* @public
*/
export const DEFAULT_BINARY_CONTENT_TYPES: (string | RegExp)[] = [
new RegExp('^image/.*$'),
new RegExp('^video/.*$'),
export const DEFAULT_BINARY_CONTENT_TYPES: string[] = [
'image/png',
'image/jpeg',
'image/jpg',
'image/avif',
'image/bmp',
'image/x-png',
'image/gif',
'image/webp',
'video/mp4',
'application/pdf',
];

Expand Down
48 changes: 24 additions & 24 deletions src/core/is-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//#region Imports

import { BinarySettings, BothValueHeaders } from '../@types';
import { getFlattenedHeadersMap, getMultiValueHeadersMap } from './headers';

//#endregion

Expand All @@ -12,7 +11,7 @@ import { getFlattenedHeadersMap, getMultiValueHeadersMap } from './headers';
*
* @example
* ```typescript
* const headers = { 'Content-Encoding': 'gzip' };
* const headers = { 'content-encoding': 'gzip' };
* const isBinary = isContentEncodingBinary(headers, ['gzip']);
* console.log(isBinary);
* // true
Expand All @@ -28,14 +27,15 @@ export function isContentEncodingBinary(
headers: BothValueHeaders,
binaryEncodingTypes: string[],
): boolean {
const multiValueHeaders = getMultiValueHeadersMap(headers);
let contentEncodings = headers['content-encoding'];

const contentEncodings = multiValueHeaders['content-encoding'];
if (!contentEncodings) return false;

if (!Array.isArray(contentEncodings)) return false;
if (!Array.isArray(contentEncodings))
contentEncodings = contentEncodings.split(',');

return contentEncodings.some(value =>
binaryEncodingTypes.some(binaryEncoding => value.includes(binaryEncoding)),
binaryEncodingTypes.includes(value.trim()),
);
}

Expand All @@ -44,7 +44,7 @@ export function isContentEncodingBinary(
*
* @example
* ```typescript
* const headers = { 'Content-Type': 'application/json' };
* const headers = { 'content-type': 'application/json' };
* const contentType = getContentType(headers);
* console.log(contentType);
* // application/json
Expand All @@ -56,20 +56,28 @@ export function isContentEncodingBinary(
* @public
*/
export function getContentType(headers: BothValueHeaders): string {
const flattenedHeaders = getFlattenedHeadersMap(headers, ';', true);
const contentTypeHeader = flattenedHeaders['content-type'] || '';
const contentTypeHeaderRaw = headers['content-type'];
const contentTypeHeader = Array.isArray(contentTypeHeaderRaw)
? contentTypeHeaderRaw[0] || ''
: contentTypeHeaderRaw || '';

if (!contentTypeHeaderRaw) return '';

// only compare mime type; ignore encoding part
return contentTypeHeader.split(';')[0];
const contentTypeStart = contentTypeHeader.indexOf(';');

if (contentTypeStart === -1) return contentTypeHeader;

return contentTypeHeader.slice(0, contentTypeStart);
}

/**
* The function that determines by the content type whether the response should be treated as binary
*
* @example
* ```typescript
* const headers = { 'Content-Type': 'image/png' };
* const isBinary = isContentTypeBinary(headers, [new RegExp('^image/.*$')]);
* const headers = { 'content-type': 'image/png' };
* const isBinary = isContentTypeBinary(headers, new Map([['image/png', true]]));
* console.log(isBinary);
* // true
* ```
Expand All @@ -82,30 +90,22 @@ export function getContentType(headers: BothValueHeaders): string {
*/
export function isContentTypeBinary(
headers: BothValueHeaders,
binaryContentTypes: (string | RegExp)[],
binaryContentTypes: string[],
) {
const binaryContentTypesRegexes = binaryContentTypes.map(binaryContentType =>
binaryContentType instanceof RegExp
? binaryContentType
: new RegExp(`${binaryContentType}`),
);

const contentType = getContentType(headers);

if (!contentType) return false;

return binaryContentTypesRegexes.some(binaryContentType =>
binaryContentType.test(contentType),
);
return binaryContentTypes.includes(contentType.trim());
}

/**
* The function used to determine from the headers and the binary settings if a response should be encoded or not
*
* @example
* ```typescript
* const headers = { 'Content-Type': 'image/png', 'Content-Encoding': 'gzip' };
* const isContentBinary = isBinary(headers, { contentEncodings: ['gzip'], contentTypes: [new RegExp('^image/.*$')] });
* const headers = { 'content-type': 'image/png', 'content-encoding': 'gzip' };
* const isContentBinary = isBinary(headers, { contentEncodings: ['gzip'], contentTypes: ['image/png'] });
* console.log(isContentBinary);
* // true
* ```
Expand Down
5 changes: 4 additions & 1 deletion src/serverless-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ export class ServerlessAdapter<
public setBinarySettings(
binarySettings: BinarySettings,
): Omit<this, 'setBinarySettings'> {
this.binarySettings = binarySettings;
this.binarySettings = {
...this.binarySettings,
...binarySettings,
};

return this;
}
Expand Down
81 changes: 44 additions & 37 deletions test/core/is-binary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,42 @@ import {
type HeaderListJest = [headers: BothValueHeaders, expectedValue: boolean][];

const headerListForContentEncodings: HeaderListJest = [
[{ 'Content-Encoding': undefined }, false],
[{ 'Content-Encoding': [] }, false],
[{ 'Content-Encoding': 'non-standard' }, false],
[{ 'Content-Encoding': ['non-standard'] }, false],
[{ 'Content-Encoding': 'gzip' }, true],
[{ 'Content-Encoding': 'deflate' }, true],
[{ 'Content-Encoding': 'br' }, true],
[{ 'Content-Encoding': 'gzip,non-standard' }, true],
[{ 'Content-Encoding': ['gzip'] }, true],
[{ 'Content-Encoding': ['gzip', 'non-standard'] }, true],
[{ 'Content-Encoding': ['deflate'] }, true],
[{ 'Content-Encoding': ['deflate', 'non-standard'] }, true],
[{ 'Content-Encoding': ['br'] }, true],
[{ 'Content-Encoding': ['br', 'non-standard'] }, true],
[{ 'content-encoding': undefined }, false],
[{ 'content-encoding': [] }, false],
[{ 'content-encoding': 'non-standard' }, false],
[{ 'content-encoding': ['non-standard'] }, false],
[{ 'content-encoding': 'gzip' }, true],
[{ 'content-encoding': 'deflate' }, true],
[{ 'content-encoding': 'br' }, true],
[{ 'content-encoding': 'gzip,non-standard' }, true],
[{ 'content-encoding': ['gzip'] }, true],
[{ 'content-encoding': ['gzip', 'non-standard'] }, true],
[{ 'content-encoding': ['deflate'] }, true],
[{ 'content-encoding': ['deflate', 'non-standard'] }, true],
[{ 'content-encoding': ['br'] }, true],
[{ 'content-encoding': ['br', 'non-standard'] }, true],
];

const headerListForContentTypes: HeaderListJest = [
[{ 'Content-Type': undefined }, false],
[{ 'Content-Type': [] }, false],
[{ 'Content-Type': 'application/json' }, false],
[{ 'Content-Type': ['application/json'] }, false],
[{ 'Content-Type': 'application/json,image/png' }, false],
[{ 'Content-Type': 'application/json;image/png' }, false],
[{ 'Content-Type': ['application/json', 'image/png'] }, false],
[{ 'Content-Type': 'image/png' }, true],
[{ 'Content-Type': ['image/png'] }, true],
[{ 'Content-Type': 'video/mp4' }, true],
[{ 'Content-Type': ['video/mp4'] }, true],
[{ 'Content-Type': 'application/pdf' }, true],
[{ 'Content-Type': ['application/pdf'] }, true],
[{ 'content-type': undefined }, false],
[{ 'content-type': [] }, false],
[{ 'content-type': 'application/json' }, false],
[{ 'content-type': ['application/json'] }, false],
[{ 'content-type': 'application/json,image/png' }, false],
[{ 'content-type': 'application/json;image/png' }, false],
[{ 'content-type': ['application/json', 'image/png'] }, false],
[{ 'content-type': 'image/png' }, true],
[{ 'content-type': ['image/png'] }, true],
[{ 'content-type': 'video/mp4' }, true],
[{ 'content-type': ['video/mp4'] }, true],
[{ 'content-type': 'application/pdf' }, true],
[{ 'content-type': ['application/pdf'] }, true],
];

describe('isContentEncodingBinary', () => {
it('should correctly check if content encoding is binary', () => {
const headersList: HeaderListJest = [
[{ 'Content-Type': 'application/json' }, false],
[{ 'content-type': 'application/json' }, false],
...headerListForContentEncodings,
];

Expand All @@ -64,19 +64,19 @@ describe('isContentEncodingBinary', () => {
describe('getContentType', () => {
it('should correctly return the content type from headers', () => {
const headersList: [headers: BothValueHeaders, expectedValue: string][] = [
[{ 'Content-Encoding': 'gzip' }, ''],
[{ 'Content-Type': 'application/json' }, 'application/json'],
[{ 'Content-Type': ['application/json'] }, 'application/json'],
[{ 'content-encoding': 'gzip' }, ''],
[{ 'content-type': 'application/json' }, 'application/json'],
[{ 'content-type': ['application/json'] }, 'application/json'],
[
{ 'Content-Type': 'application/json,image/png' },
{ 'content-type': 'application/json,image/png' },
'application/json,image/png',
],
[{ 'Content-Type': 'application/json;image/png' }, 'application/json'],
[{ 'content-type': 'application/json;image/png' }, 'application/json'],
[
{ 'Content-Type': ['application/json', 'image/png'] },
{ 'content-type': ['application/json', 'image/png'] },
'application/json',
],
[{ 'Content-Type': ['image/png', 'application/json'] }, 'image/png'],
[{ 'content-type': ['image/png', 'application/json'] }, 'image/png'],
];

for (const [headers, expectedValue] of headersList) {
Expand All @@ -90,7 +90,7 @@ describe('getContentType', () => {
describe('isContentTypeBinary', () => {
it('should correctly check if content type is binary', () => {
const headersList: [headers: BothValueHeaders, expectedValue: boolean][] = [
[{ 'Content-Encoding': 'gzip' }, false],
[{ 'content-encoding': 'gzip' }, false],
...headerListForContentTypes,
];

Expand Down Expand Up @@ -121,7 +121,14 @@ describe('isBinary', () => {
contentEncodings,
});

expect(isContentBinary).toBe(expectedValue);
expect(
isContentBinary,
`contentTypes: ${contentTypes.join(
';',
)}, contentEncodings: ${contentEncodings.join(
';',
)}: has ${expectedValue} inside ${JSON.stringify(headers)}`,
).toBe(expectedValue);
}
});

Expand Down
2 changes: 1 addition & 1 deletion test/serverless-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('ServerlessAdapter', () => {
framework,
[adapter],
resolver,
binarySettings,
expect.objectContaining(binarySettings),
respondWithErrors,
logger,
);
Expand Down

0 comments on commit 4fb3a39

Please sign in to comment.