From 34ff6991dab5f7a5f1968b824a45769cc1b4bd3a Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 16 Sep 2026 22:35:13 +0600 Subject: [PATCH 1/6] feat(event-handler): refactor compression encoding acceptance logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract quality value parsing into dedicated `getQuality()` function to handle `q` parameter extraction from Accept-Encoding codings - Add `acceptsEncoding()` function to properly evaluate client encoding preferences according to RFC 9110 §8.4.1.3 - Handle `x-gzip` alias normalization to canonical GZIP encoding type - Implement wildcard (`*`) precedence logic where exact coding matches take priority over wildcards - Replace inline string inclusion checks with compliant quality-based acceptance logic - Improve handling of missing Accept-Encoding headers and non-finite quality values - Simplifies `shouldCompress()` by delegating encoding negotiation to dedicated function --- .../src/http/middleware/compress.ts | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/event-handler/src/http/middleware/compress.ts b/packages/event-handler/src/http/middleware/compress.ts index 26ceb5564f..c970fec4fd 100644 --- a/packages/event-handler/src/http/middleware/compress.ts +++ b/packages/event-handler/src/http/middleware/compress.ts @@ -90,14 +90,57 @@ const compress = (options?: CompressionOptions): Middleware => { }; }; +/** + * Gets the quality value from an Accept-Encoding coding's parameters. + * + * Missing `q` defaults to 1; non-finite values return 0. Callers treat non-positive values as not accepted. + * + * @param parameters - The coding parameters to inspect + */ +const getQuality = (parameters: string[]): number => { + for (const parameter of parameters) { + const [name, value] = parameter.split('='); + if (name.trim().toLowerCase() === 'q') { + const quality = Number(value); + return Number.isFinite(quality) ? quality : 0; + } + } + return 1; +}; + +/** + * Checks if the client accepts the preferred compression encoding based on the Accept-Encoding header. + * + * @param header - The value of the Accept-Encoding header from the request + * @param preferredEncoding - The preferred compression encoding to use + */ +const acceptsEncoding = ( + header: string | null, + preferredEncoding: NonNullable +): boolean => { + if (header === null) return true; + + // An exact coding match takes precedence over the `*` wildcard + let acceptedByWildcard = false; + for (const entry of header.split(',')) { + const [rawCoding, ...parameters] = entry.split(';'); + let coding = rawCoding.trim().toLowerCase(); + if (coding === 'x-gzip') coding = COMPRESSION_ENCODING_TYPES.GZIP; // RFC 9110 §8.4.1.3 alias + const accepted = getQuality(parameters) > 0; + + if (coding === preferredEncoding) return accepted; + if (coding === COMPRESSION_ENCODING_TYPES.ANY) + acceptedByWildcard = accepted; + } + return acceptedByWildcard; +}; + const shouldCompress = ( request: Request, response: Response, preferredEncoding: NonNullable, threshold: NonNullable ): response is Response & { body: NonNullable } => { - const acceptedEncoding = - request.headers.get('accept-encoding') ?? COMPRESSION_ENCODING_TYPES.ANY; const contentLength = response.headers.get('content-length'); const cacheControl = response.headers.get('cache-control'); @@ -105,10 +148,10 @@ const shouldCompress = ( response.headers.has('content-encoding') || response.headers.has('transfer-encoding'); - const shouldEncode = - !acceptedEncoding.includes(COMPRESSION_ENCODING_TYPES.IDENTITY) && - (acceptedEncoding.includes(preferredEncoding) || - acceptedEncoding.includes(COMPRESSION_ENCODING_TYPES.ANY)); + const shouldEncode = acceptsEncoding( + request.headers.get('accept-encoding'), + preferredEncoding + ); return ( shouldEncode && From 0224c8e5cb4d4c923e287bb928bb7dbaaa0b7d17 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 16 Sep 2026 22:35:27 +0600 Subject: [PATCH 2/6] test(event-handler): add comprehensive Accept-Encoding negotiation tests - Add test for gzip with q=0 quality factor to verify no compression - Add test for multiple encodings (gzip, identity) to verify compression - Add test for missing Accept-Encoding header to verify default compression - Add test for empty Accept-Encoding header to verify no compression - Add parametrized tests covering 20+ edge cases including: * Case-insensitive encoding names (GZIP, x-gzip) * Quality factor parsing (q=0, q=0.5, q=1.0, invalid values) * Whitespace handling and malformed values * Wildcard matching with quality factors - Add test for deflate encoding negotiation - Add test to verify no negotiation down to alternative encodings - Ensure comprehensive coverage of HTTP Accept-Encoding RFC compliance --- .../unit/http/middleware/compress.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/packages/event-handler/tests/unit/http/middleware/compress.test.ts b/packages/event-handler/tests/unit/http/middleware/compress.test.ts index 7d05a35465..85625f1312 100644 --- a/packages/event-handler/tests/unit/http/middleware/compress.test.ts +++ b/packages/event-handler/tests/unit/http/middleware/compress.test.ts @@ -236,6 +236,154 @@ describe('Compress Middleware', () => { expect(result.isBase64Encoded).toBe(false); }); + it('does not compress if Accept-Encoding includes gzip with q=0', async () => { + // Prepare + const noCompressionEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': 'gzip;q=0', + }); + app.get('/test', () => body); + + // Act + const result = await app.resolve(noCompressionEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBeUndefined(); + expect(result.isBase64Encoded).toBe(false); + }); + + it('compresses if Accept-Encoding includes gzip and identity', async () => { + // Prepare + const compressionEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': 'gzip, identity', + }); + app.get('/test', () => body); + + // Act + const result = await app.resolve(compressionEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBe('gzip'); + expect(result.isBase64Encoded).toBe(true); + }); + + it('compresses when Accept-Encoding is absent', async () => { + // Prepare + const noHeaderEvent = createTestEvent('/test', 'GET'); + app.get('/test', () => body); + + // Act + const result = await app.resolve(noHeaderEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBe('gzip'); + expect(result.isBase64Encoded).toBe(true); + }); + + it('does not compress when Accept-Encoding is empty', async () => { + // Prepare + const noCompressionEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': '', + }); + app.get('/test', () => body); + + // Act + const result = await app.resolve(noCompressionEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBeUndefined(); + expect(result.isBase64Encoded).toBe(false); + }); + + it.each([ + { expectedEncoding: 'gzip', header: 'GZIP' }, + { expectedEncoding: 'gzip', header: 'x-gzip' }, + { expectedEncoding: undefined, header: 'x-gzip;q=0, *;q=1' }, + { expectedEncoding: undefined, header: 'gzip;Q=0' }, + { expectedEncoding: 'gzip', header: ' gzip ; q=0.5 , identity ' }, + { expectedEncoding: undefined, header: ' gzip ; q=0 ' }, + { expectedEncoding: undefined, header: 'gzipx, x-deflate' }, + { expectedEncoding: 'gzip', header: ', ,gzip' }, + { expectedEncoding: undefined, header: 'gzip;q=' }, + { expectedEncoding: undefined, header: 'gzip;q=invalid' }, + { expectedEncoding: undefined, header: 'gzip;q=Infinity' }, + { expectedEncoding: 'gzip', header: 'gzip;q=1.1' }, + { expectedEncoding: undefined, header: 'gzip;q=-1' }, + { expectedEncoding: 'gzip', header: 'gzip;q=0.5' }, + { expectedEncoding: 'gzip', header: 'gzip;q=1.0000' }, + { expectedEncoding: 'gzip', header: 'gzip;foo=bar' }, + { expectedEncoding: 'gzip', header: 'gzip;foo=bar;q=1' }, + { expectedEncoding: undefined, header: 'gzip;q=0, *;q=1' }, + { expectedEncoding: undefined, header: 'gzip;q=invalid, *;q=1' }, + { expectedEncoding: undefined, header: '*;q=0' }, + { expectedEncoding: 'gzip', header: 'identity;q=0, *;q=1' }, + ])( + 'resolves encoding $expectedEncoding for Accept-Encoding "$header"', + async ({ expectedEncoding, header }) => { + // Prepare + const acceptEncodingEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': header, + }); + app.get('/test', () => body); + + // Act + const result = await app.resolve(acceptEncodingEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBe(expectedEncoding); + expect(result.isBase64Encoded).toBe(!!expectedEncoding); + } + ); + + it('compresses with deflate when Accept-Encoding accepts deflate', async () => { + // Prepare + const application = new Router(); + application.get( + '/test', + [ + compress({ encoding: 'deflate' }), + createSettingHeadersMiddleware({ + 'content-length': '2000', + }), + ], + () => body + ); + const deflateEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': 'deflate;q=0.5', + }); + + // Act + const result = await application.resolve(deflateEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBe('deflate'); + expect(result.isBase64Encoded).toBe(true); + }); + + it('does not negotiate down to an accepted encoding other than the configured one', async () => { + // Prepare + const application = new Router(); + application.get( + '/test', + [ + compress({ encoding: 'deflate' }), + createSettingHeadersMiddleware({ + 'content-length': '2000', + }), + ], + () => body + ); + const gzipOnlyEvent = createTestEvent('/test', 'GET', { + 'Accept-Encoding': 'gzip', + }); + + // Act + const result = await application.resolve(gzipOnlyEvent, context); + + // Assess + expect(result.headers?.['content-encoding']).toBeUndefined(); + expect(result.isBase64Encoded).toBe(false); + }); + it('skips compression and content-length in streaming mode', async () => { // Prepare const application = new Router(); From cae6b684849bfb869d5e5632204c8fc0b56cf1da Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Wed, 16 Sep 2026 22:43:34 +0600 Subject: [PATCH 3/6] test(event-handler): improve compress middleware test descriptions --- .../tests/unit/http/middleware/compress.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/event-handler/tests/unit/http/middleware/compress.test.ts b/packages/event-handler/tests/unit/http/middleware/compress.test.ts index 85625f1312..0c0671d1b1 100644 --- a/packages/event-handler/tests/unit/http/middleware/compress.test.ts +++ b/packages/event-handler/tests/unit/http/middleware/compress.test.ts @@ -236,7 +236,7 @@ describe('Compress Middleware', () => { expect(result.isBase64Encoded).toBe(false); }); - it('does not compress if Accept-Encoding includes gzip with q=0', async () => { + it('does not compress when Accept-Encoding includes gzip with q=0', async () => { // Prepare const noCompressionEvent = createTestEvent('/test', 'GET', { 'Accept-Encoding': 'gzip;q=0', @@ -251,7 +251,7 @@ describe('Compress Middleware', () => { expect(result.isBase64Encoded).toBe(false); }); - it('compresses if Accept-Encoding includes gzip and identity', async () => { + it('compresses when Accept-Encoding includes gzip and identity', async () => { // Prepare const compressionEvent = createTestEvent('/test', 'GET', { 'Accept-Encoding': 'gzip, identity', @@ -297,6 +297,7 @@ describe('Compress Middleware', () => { it.each([ { expectedEncoding: 'gzip', header: 'GZIP' }, { expectedEncoding: 'gzip', header: 'x-gzip' }, + { expectedEncoding: 'gzip', header: 'x-Gzip' }, { expectedEncoding: undefined, header: 'x-gzip;q=0, *;q=1' }, { expectedEncoding: undefined, header: 'gzip;Q=0' }, { expectedEncoding: 'gzip', header: ' gzip ; q=0.5 , identity ' }, From 8a01be63916e47c6a669d0ac9798b0f9acd20035 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Thu, 17 Sep 2026 18:58:30 +0600 Subject: [PATCH 4/6] fix(event-handler): parse Accept-Encoding q-values leniently - Parse q-values as finite numbers and clamp them to [0, 1] instead of enforcing the strict RFC 9110 grammar - Treat unparseable q-values as 0 - Compare the configured encoding's quality against identity and prefer compression on ties - Keep q-parameter regexes private to the compress middleware - Consolidate deflate test setup into a createDeflateApp helper - Expand test coverage for q-value edge cases and encoding preference --- .../src/http/middleware/compress.ts | 49 ++++-- .../unit/http/middleware/compress.test.ts | 152 +++++++++--------- 2 files changed, 111 insertions(+), 90 deletions(-) diff --git a/packages/event-handler/src/http/middleware/compress.ts b/packages/event-handler/src/http/middleware/compress.ts index c970fec4fd..95189f71b3 100644 --- a/packages/event-handler/src/http/middleware/compress.ts +++ b/packages/event-handler/src/http/middleware/compress.ts @@ -93,23 +93,36 @@ const compress = (options?: CompressionOptions): Middleware => { /** * Gets the quality value from an Accept-Encoding coding's parameters. * - * Missing `q` defaults to 1; non-finite values return 0. Callers treat non-positive values as not accepted. + * Missing `q` defaults to 1; finite numbers are clamped to [0, 1]; anything else returns 0. + * + * Quality values: https://www.rfc-editor.org/rfc/rfc9110.html#section-12.4.2 * * @param parameters - The coding parameters to inspect */ const getQuality = (parameters: string[]): number => { for (const parameter of parameters) { - const [name, value] = parameter.split('='); - if (name.trim().toLowerCase() === 'q') { - const quality = Number(value); - return Number.isFinite(quality) ? quality : 0; - } + const separator = parameter.indexOf('='); + const name = separator === -1 ? parameter : parameter.slice(0, separator); + + if (name.trim().toLowerCase() !== 'q') continue; + if (separator === -1) return 0; + + const value = parameter.slice(separator + 1).trim(); + if (value.length === 0) return 0; + + const quality = Number(value); + if (!Number.isFinite(quality)) return 0; + + return Math.min(Math.max(quality, 0), 1); } + return 1; }; /** - * Checks if the client accepts the preferred compression encoding based on the Accept-Encoding header. + * Checks whether the preferred compression encoding is at least as acceptable as identity. + * + * When both have the same quality, the server preference for compression wins. * * @param header - The value of the Accept-Encoding header from the request * @param preferredEncoding - The preferred compression encoding to use @@ -120,19 +133,27 @@ const acceptsEncoding = ( ): boolean => { if (header === null) return true; - // An exact coding match takes precedence over the `*` wildcard - let acceptedByWildcard = false; + // Exact coding matches take precedence over the `*` wildcard. + let preferredQuality: number | undefined; + let identityQuality: number | undefined; + let wildcardQuality: number | undefined; for (const entry of header.split(',')) { const [rawCoding, ...parameters] = entry.split(';'); let coding = rawCoding.trim().toLowerCase(); if (coding === 'x-gzip') coding = COMPRESSION_ENCODING_TYPES.GZIP; // RFC 9110 §8.4.1.3 alias - const accepted = getQuality(parameters) > 0; + const quality = getQuality(parameters); - if (coding === preferredEncoding) return accepted; - if (coding === COMPRESSION_ENCODING_TYPES.ANY) - acceptedByWildcard = accepted; + if (coding === preferredEncoding) preferredQuality ??= quality; + if (coding === COMPRESSION_ENCODING_TYPES.IDENTITY) + identityQuality ??= quality; + if (coding === COMPRESSION_ENCODING_TYPES.ANY) wildcardQuality ??= quality; } - return acceptedByWildcard; + + const compressionQuality = preferredQuality ?? wildcardQuality ?? 0; + const uncompressedQuality = + identityQuality ?? wildcardQuality ?? compressionQuality; + + return compressionQuality > 0 && compressionQuality >= uncompressedQuality; }; const shouldCompress = ( diff --git a/packages/event-handler/tests/unit/http/middleware/compress.test.ts b/packages/event-handler/tests/unit/http/middleware/compress.test.ts index 0c0671d1b1..efb2f10e63 100644 --- a/packages/event-handler/tests/unit/http/middleware/compress.test.ts +++ b/packages/event-handler/tests/unit/http/middleware/compress.test.ts @@ -15,6 +15,19 @@ describe('Compress Middleware', () => { let app: Router; const body = { test: 'x'.repeat(2000) }; + const createDeflateApp = () => { + const application = new Router(); + application.get( + '/test', + [ + compress({ encoding: 'deflate' }), + createSettingHeadersMiddleware({ 'content-length': '2000' }), + ], + () => body + ); + return application; + }; + beforeEach(() => { app = new Router(); app.use(compress()); @@ -247,6 +260,7 @@ describe('Compress Middleware', () => { const result = await app.resolve(noCompressionEvent, context); // Assess + expect(result.statusCode).toBe(200); expect(result.headers?.['content-encoding']).toBeUndefined(); expect(result.isBase64Encoded).toBe(false); }); @@ -266,60 +280,62 @@ describe('Compress Middleware', () => { expect(result.isBase64Encoded).toBe(true); }); - it('compresses when Accept-Encoding is absent', async () => { - // Prepare - const noHeaderEvent = createTestEvent('/test', 'GET'); - app.get('/test', () => body); - - // Act - const result = await app.resolve(noHeaderEvent, context); - - // Assess - expect(result.headers?.['content-encoding']).toBe('gzip'); - expect(result.isBase64Encoded).toBe(true); - }); - - it('does not compress when Accept-Encoding is empty', async () => { - // Prepare - const noCompressionEvent = createTestEvent('/test', 'GET', { - 'Accept-Encoding': '', - }); - app.get('/test', () => body); - - // Act - const result = await app.resolve(noCompressionEvent, context); - - // Assess - expect(result.headers?.['content-encoding']).toBeUndefined(); - expect(result.isBase64Encoded).toBe(false); - }); - it.each([ - { expectedEncoding: 'gzip', header: 'GZIP' }, - { expectedEncoding: 'gzip', header: 'x-gzip' }, - { expectedEncoding: 'gzip', header: 'x-Gzip' }, - { expectedEncoding: undefined, header: 'x-gzip;q=0, *;q=1' }, - { expectedEncoding: undefined, header: 'gzip;Q=0' }, - { expectedEncoding: 'gzip', header: ' gzip ; q=0.5 , identity ' }, - { expectedEncoding: undefined, header: ' gzip ; q=0 ' }, - { expectedEncoding: undefined, header: 'gzipx, x-deflate' }, - { expectedEncoding: 'gzip', header: ', ,gzip' }, - { expectedEncoding: undefined, header: 'gzip;q=' }, - { expectedEncoding: undefined, header: 'gzip;q=invalid' }, - { expectedEncoding: undefined, header: 'gzip;q=Infinity' }, - { expectedEncoding: 'gzip', header: 'gzip;q=1.1' }, - { expectedEncoding: undefined, header: 'gzip;q=-1' }, - { expectedEncoding: 'gzip', header: 'gzip;q=0.5' }, - { expectedEncoding: 'gzip', header: 'gzip;q=1.0000' }, - { expectedEncoding: 'gzip', header: 'gzip;foo=bar' }, - { expectedEncoding: 'gzip', header: 'gzip;foo=bar;q=1' }, - { expectedEncoding: undefined, header: 'gzip;q=0, *;q=1' }, - { expectedEncoding: undefined, header: 'gzip;q=invalid, *;q=1' }, - { expectedEncoding: undefined, header: '*;q=0' }, - { expectedEncoding: 'gzip', header: 'identity;q=0, *;q=1' }, + { compressed: false, header: '' }, + { compressed: true, header: 'GZIP' }, + { compressed: true, header: 'x-gzip' }, + { compressed: true, header: 'x-Gzip' }, + { compressed: false, header: 'x-gzip;q=0, *;q=1' }, + { compressed: false, header: 'gzip;Q=0' }, + { + compressed: false, + header: 'gzip ; q=0.5 , identity', + }, + { + compressed: true, + header: 'gzip;q=1, identity;q=0.1', + }, + { + compressed: true, + header: 'gzip;q=0.5, identity;q=0.5', + }, + { compressed: false, header: 'gzip ; q=0' }, + { compressed: false, header: 'gzipx, x-deflate' }, + { compressed: true, header: ', ,gzip' }, + { compressed: false, header: 'gzip;q=' }, + { compressed: false, header: 'gzip;q=invalid' }, + { compressed: false, header: 'gzip;q=Infinity' }, + { compressed: true, header: 'gzip;q=1.1' }, + { compressed: false, header: 'gzip;q=-1' }, + { compressed: true, header: 'gzip;q=0.5' }, + { compressed: true, header: 'gzip;\tq=0.5\t' }, + { compressed: true, header: 'gzip;q=1.0000' }, + { compressed: true, header: 'gzip;q=1e-1' }, + { compressed: true, header: 'gzip;q=0.1234' }, + { compressed: true, header: 'gzip;q= 0.5' }, + { compressed: true, header: 'gzip;q = 0.5' }, + { compressed: true, header: 'gzip;q=.5' }, + { compressed: false, header: 'gzip;q=1foo' }, + { compressed: false, header: 'gzip;q=NaN' }, + { compressed: false, header: 'gzip;q=0.5=bad' }, + { compressed: false, header: 'gzip;q =0' }, + { compressed: false, header: 'gzip;q' }, + { compressed: false, header: 'gzip;q=0..' }, + { compressed: false, header: 'gzip;q=0.' }, + { compressed: true, header: 'gzip;q=1.' }, + { compressed: true, header: 'gzip;q=1.000' }, + { compressed: true, header: 'gzip;q=0.001' }, + { compressed: true, header: 'gzip;foo=bar' }, + { compressed: true, header: 'gzip;foo=bar;q=1' }, + { compressed: false, header: 'gzip;q=0, *;q=1' }, + { compressed: false, header: 'gzip;q=invalid, *;q=1' }, + { compressed: false, header: '*;q=0' }, + { compressed: true, header: 'identity;q=0, *;q=1' }, + { compressed: false, header: 'gzip;q=0, gzip' }, + { compressed: false, header: '*;q=0, *' }, ])( - 'resolves encoding $expectedEncoding for Accept-Encoding "$header"', - async ({ expectedEncoding, header }) => { + 'returns compressed: $compressed for Accept-Encoding "$header"', + async ({ compressed, header }) => { // Prepare const acceptEncodingEvent = createTestEvent('/test', 'GET', { 'Accept-Encoding': header, @@ -330,24 +346,17 @@ describe('Compress Middleware', () => { const result = await app.resolve(acceptEncodingEvent, context); // Assess - expect(result.headers?.['content-encoding']).toBe(expectedEncoding); - expect(result.isBase64Encoded).toBe(!!expectedEncoding); + expect(result.statusCode).toBe(200); + expect(result.headers?.['content-encoding']).toBe( + compressed ? 'gzip' : undefined + ); + expect(result.isBase64Encoded).toBe(compressed); } ); it('compresses with deflate when Accept-Encoding accepts deflate', async () => { // Prepare - const application = new Router(); - application.get( - '/test', - [ - compress({ encoding: 'deflate' }), - createSettingHeadersMiddleware({ - 'content-length': '2000', - }), - ], - () => body - ); + const application = createDeflateApp(); const deflateEvent = createTestEvent('/test', 'GET', { 'Accept-Encoding': 'deflate;q=0.5', }); @@ -362,17 +371,7 @@ describe('Compress Middleware', () => { it('does not negotiate down to an accepted encoding other than the configured one', async () => { // Prepare - const application = new Router(); - application.get( - '/test', - [ - compress({ encoding: 'deflate' }), - createSettingHeadersMiddleware({ - 'content-length': '2000', - }), - ], - () => body - ); + const application = createDeflateApp(); const gzipOnlyEvent = createTestEvent('/test', 'GET', { 'Accept-Encoding': 'gzip', }); @@ -381,6 +380,7 @@ describe('Compress Middleware', () => { const result = await application.resolve(gzipOnlyEvent, context); // Assess + expect(result.statusCode).toBe(200); expect(result.headers?.['content-encoding']).toBeUndefined(); expect(result.isBase64Encoded).toBe(false); }); From e56066a77b16e6e1f74c7ec6b9d031ff4cd9b1f6 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Thu, 17 Sep 2026 19:09:32 +0600 Subject: [PATCH 5/6] test(event-handler): add status code assertions to compress middleware tests to match others --- .../event-handler/tests/unit/http/middleware/compress.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/event-handler/tests/unit/http/middleware/compress.test.ts b/packages/event-handler/tests/unit/http/middleware/compress.test.ts index efb2f10e63..eb83fc8f38 100644 --- a/packages/event-handler/tests/unit/http/middleware/compress.test.ts +++ b/packages/event-handler/tests/unit/http/middleware/compress.test.ts @@ -245,6 +245,7 @@ describe('Compress Middleware', () => { const result = await app.resolve(noCompressionEvent, context); // Assess + expect(result.statusCode).toBe(200); expect(result.headers?.['content-encoding']).toBeUndefined(); expect(result.isBase64Encoded).toBe(false); }); @@ -276,6 +277,7 @@ describe('Compress Middleware', () => { const result = await app.resolve(compressionEvent, context); // Assess + expect(result.statusCode).toBe(200); expect(result.headers?.['content-encoding']).toBe('gzip'); expect(result.isBase64Encoded).toBe(true); }); @@ -365,6 +367,7 @@ describe('Compress Middleware', () => { const result = await application.resolve(deflateEvent, context); // Assess + expect(result.statusCode).toBe(200); expect(result.headers?.['content-encoding']).toBe('deflate'); expect(result.isBase64Encoded).toBe(true); }); From 9d16cb8be6262d2a6b4645999e346b4d60026496 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Date: Tue, 22 Sep 2026 10:19:50 +0600 Subject: [PATCH 6/6] fix(event-handler): validate q-values as plain decimals in Accept-Encoding - Add DECIMAL_QVALUE_REGEX constant to validate q-values format - Update quality value parsing to reject scientific notation (e.g., `1e-1`) and alternative number bases (e.g., `0x1`, `0o1`, `0b1`) - Simplify getQuality logic by validating format before parsing instead of checking finiteness after - Update JSDoc to clarify that only plain decimals are accepted - Add test cases for invalid q-value formats that were previously accepted - Add test cases for multiple encodings where lower priority options should be used --- packages/event-handler/src/http/constants.ts | 3 +++ packages/event-handler/src/http/middleware/compress.ts | 10 ++++------ .../tests/unit/http/middleware/compress.test.ts | 7 ++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/event-handler/src/http/constants.ts b/packages/event-handler/src/http/constants.ts index 52eda51c41..58721d5397 100644 --- a/packages/event-handler/src/http/constants.ts +++ b/packages/event-handler/src/http/constants.ts @@ -109,6 +109,8 @@ const DEFAULT_COMPRESSION_RESPONSE_THRESHOLD = 1024; const CACHE_CONTROL_NO_TRANSFORM_REGEX = /(?:^|,)\s*?no-transform\s*?(?:,|$)/i; +const DECIMAL_QVALUE_REGEX = /^(?:\d+(?:\.\d*)?|\.\d+)$/; + const COMPRESSION_ENCODING_TYPES = { GZIP: 'gzip', DEFLATE: 'deflate', @@ -207,6 +209,7 @@ const HttpStatusText: Record = { export { CACHE_CONTROL_NO_TRANSFORM_REGEX, COMPRESSION_ENCODING_TYPES, + DECIMAL_QVALUE_REGEX, DEFAULT_COMPRESSION_RESPONSE_THRESHOLD, DEFAULT_CORS_OPTIONS, HttpStatusCodes, diff --git a/packages/event-handler/src/http/middleware/compress.ts b/packages/event-handler/src/http/middleware/compress.ts index 95189f71b3..e7b7e08acb 100644 --- a/packages/event-handler/src/http/middleware/compress.ts +++ b/packages/event-handler/src/http/middleware/compress.ts @@ -3,6 +3,7 @@ import type { Middleware } from '../../types/index.js'; import { CACHE_CONTROL_NO_TRANSFORM_REGEX, COMPRESSION_ENCODING_TYPES, + DECIMAL_QVALUE_REGEX, DEFAULT_COMPRESSION_RESPONSE_THRESHOLD, } from '../constants.js'; @@ -93,7 +94,7 @@ const compress = (options?: CompressionOptions): Middleware => { /** * Gets the quality value from an Accept-Encoding coding's parameters. * - * Missing `q` defaults to 1; finite numbers are clamped to [0, 1]; anything else returns 0. + * Missing `q` defaults to 1; plain decimals (e.g. `1`, `0.5`, `.5`) are capped at 1; anything else returns 0. * * Quality values: https://www.rfc-editor.org/rfc/rfc9110.html#section-12.4.2 * @@ -108,12 +109,9 @@ const getQuality = (parameters: string[]): number => { if (separator === -1) return 0; const value = parameter.slice(separator + 1).trim(); - if (value.length === 0) return 0; + if (!DECIMAL_QVALUE_REGEX.test(value)) return 0; - const quality = Number(value); - if (!Number.isFinite(quality)) return 0; - - return Math.min(Math.max(quality, 0), 1); + return Math.min(Number(value), 1); } return 1; diff --git a/packages/event-handler/tests/unit/http/middleware/compress.test.ts b/packages/event-handler/tests/unit/http/middleware/compress.test.ts index eb83fc8f38..8061bef16b 100644 --- a/packages/event-handler/tests/unit/http/middleware/compress.test.ts +++ b/packages/event-handler/tests/unit/http/middleware/compress.test.ts @@ -312,7 +312,10 @@ describe('Compress Middleware', () => { { compressed: true, header: 'gzip;q=0.5' }, { compressed: true, header: 'gzip;\tq=0.5\t' }, { compressed: true, header: 'gzip;q=1.0000' }, - { compressed: true, header: 'gzip;q=1e-1' }, + { compressed: false, header: 'gzip;q=1e-1' }, + { compressed: false, header: 'gzip;q=0x1' }, + { compressed: false, header: 'gzip;q=0o1' }, + { compressed: false, header: 'gzip;q=0b1' }, { compressed: true, header: 'gzip;q=0.1234' }, { compressed: true, header: 'gzip;q= 0.5' }, { compressed: true, header: 'gzip;q = 0.5' }, @@ -335,6 +338,8 @@ describe('Compress Middleware', () => { { compressed: true, header: 'identity;q=0, *;q=1' }, { compressed: false, header: 'gzip;q=0, gzip' }, { compressed: false, header: '*;q=0, *' }, + { compressed: true, header: '*;q=0, gzip' }, + { compressed: true, header: 'identity;q=0, gzip' }, ])( 'returns compressed: $compressed for Accept-Encoding "$header"', async ({ compressed, header }) => {