Skip to content

fix(event-handler): honor Accept-Encoding q-values in compression - #5725

Open
arnabrahman wants to merge 5 commits into
aws-powertools:mainfrom
arnabrahman:5691-fix-compress-bug
Open

arnabrahman wants to merge 5 commits into
aws-powertools:mainfrom
arnabrahman:5691-fix-compress-bug

Conversation

@arnabrahman

Copy link
Copy Markdown
Contributor

Summary

The HTTP compression middleware matched Accept-Encoding with substring checks, so gzip;q=0 was still compressed, any mention of identity (even identity;q=0) disabled compression, and GZIP was not recognized. This PR parses the header as a list of codings with RFC 9110 quality values and compresses only when the configured encoding is at least as preferred as identity.

Changes

  • Parse encoding names as exact, case-insensitive tokens and support x-gzip as an alias for gzip (RFC 9110 §8.4.1.3).
  • Parse q-values as finite numeric preferences, clamped to [0, 1]; a missing q defaults to 1.
  • Treat empty or non-numeric values (e.g. q=, q=abc, q=1foo, q=Infinity) as q=0, so an unreadable preference never counts as consent to compress.
  • If the header names the encoding explicitly, use its q-value instead of *, even when it's q=0 (e.g. gzip;q=0, * is not compressed, *;q=0, gzip is).
  • Compare the configured encoding with identity: use the higher q-value and prefer compression when their qualities tie (e.g. gzip, identity).
  • Behavior changes for existing clients: gzip;q=0 and unparseable q-values are no longer compressed; gzip, identity, identity;q=0, gzip, and GZIP are now compressed.
  • Preserve existing behavior for absent and empty Accept-Encoding headers.
  • Add regression coverage for casing, whitespace, aliases, malformed and boundary q-values, wildcard precedence, identity preference, and configured deflate behavior.
  • Assert successful response status in table-driven negotiation tests so an internal error cannot satisfy a no-compression expectation.
q-value parsing approach — maintainer input requested

I was initially undecided between strict RFC 9110 parsing and accepting a broader range of numeric values, so I compared how other HTTP stacks handle q-values:

Implementation q-value parsing Invalid or out-of-range q-value
Hono Number(), clamped to [0, 1], q/Q Unparseable → 1 (literal NaN0)
Express compression (negotiator@0.6.4) parseFloat(), not clamped, lowercase q only NaN → coding dropped; q=5 stays 5; 0.5abc0.5
nginx (ngx_http_gzip_quantity) Strict: 0 or 1, optional . and up to 3 digits, q/Q Treated as q=0, so no gzip
ASP.NET Core (TryParseQualityDouble) Strict RFC 9110 grammar, q/Q, whitespace allowed around = Coding dropped from the list
tower-http (QValue::parse) Strict RFC 9110 grammar, q/Q, no whitespace around = Coding dropped from the list
This PR Number(), clamped to [0, 1], q/Q, whitespace allowed Unparseable → 0

The comparison shows a split: the JavaScript frameworks are lenient, while nginx, ASP.NET Core, and tower-http are strict. I chose the lenient approach because this is a TypeScript/JavaScript library and it accepts headers that users may already see work with Express or Hono, including .5, 1.0000, and q = 0.5. Unreadable values still behave as q=0, so they do not enable compression.

This is a compatibility choice rather than a strong preference. Strict RFC 9110 parsing would also be reasonable and would be a small, localized change to getQuality and its tests. I’d appreciate input on which approach the project should prefer.

Issue number: closes #5691


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

- 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
- 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
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L PRs between 100-499 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: compress() ignores Accept-Encoding qvalues and skips compression when identity is listed

1 participant