Skip to content

Parse Accept-Encoding q-values in withGzip - #130

Merged
LarsLaskowski merged 1 commit into
mainfrom
claude/issue-101-0ofd4t
Aug 23, 2026
Merged

Parse Accept-Encoding q-values in withGzip#130
LarsLaskowski merged 1 commit into
mainfrom
claude/issue-101-0ofd4t

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Pull Request

📖 Description

withGzip decided whether to compress with a substring search:

if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {

But Accept-Encoding is a q-value list (RFC 9110 §12.5.3), not an opaque string. Two bugs followed from that, both verified to still reproduce on main (f5ceab5) before this fix:

  1. gzip;q=0 is an explicit refusal, but the substring match compressed anyway — the client received a body it had just told the server it could not decode. Same for identity, gzip;q=0 and gzip;q=0.000.
  2. x-gzip matched as a false positive. A deprecated alias most clients happen to handle, so mostly cosmetic, but the same bug would fire for any future coding token containing gzip.

This is a fix, not a breaking change: the JSON response shape is untouched, so no /api/v2 is needed.

Behaviour change worth calling out

Accept-Encoding: * now yields a compressed response; previously the substring match didn't see gzip in * and returned identity. This is what the header means (a client sending * accepts any coding) and what most servers do, so it is compatible — but it is a behaviour addition rather than a pure bug fix.

*;q=0 correctly stays a refusal.

One deviation from the issue's suggested patch

The issue's sketch iterates and continues past a refused gzip entry, which means Accept-Encoding: gzip;q=0, * would fall through to the wildcard and compress — still wrong. RFC 9110 §12.5.3 says the most specific match wins, so this implementation lets an explicit gzip entry decide and only falls back to the wildcard when no gzip entry is present. Covered by tests in both directions (gzip;q=0, * → no; gzip, *;q=0 → yes).

A qvalue that fails to parse is ignored rather than read as a refusal, so a malformed header keeps the pre-existing compressing behaviour instead of silently losing compression.

🎫 Issues

Closes #101

👩‍💻 Reviewer Notes

Two small helpers added to internal/httpapi/middleware.go (acceptsGzip, qualityIsZero) and a one-line change at the withGzip call site; strconv added to the imports. Nothing else in the middleware chain is touched.

Worth a look at the wildcard-precedence logic in acceptsGzip (the deviation described above) and at the decision to treat an unparseable qvalue as "not a refusal".

Smoke test against a running instance:

curl -s -H 'Accept-Encoding: gzip;q=0' -D- -o /dev/null http://localhost:8080/api/v1/metrics   # no Content-Encoding
curl -s -H 'Accept-Encoding: x-gzip'   -D- -o /dev/null http://localhost:8080/api/v1/metrics   # no Content-Encoding
curl -s -H 'Accept-Encoding: gzip'     -D- -o /dev/null http://localhost:8080/api/v1/metrics   # Content-Encoding: gzip

No Pi hardware is required to verify this change — it is pure HTTP header handling, fully covered by the tests below.

📑 Test Plan

Both test styles the issue suggested, added to internal/httpapi/middleware_test.go:

  • TestWithGzip_AcceptEncodingNegotiation — table-driven through the full handler chain (s.Handler().ServeHTTP), asserting on the Content-Encoding response header, matching the style of the existing TestHandleHistory_GzipWhenAccepted. Covers every row from the issue's table plus uppercase codings and both wildcard-precedence cases.
  • TestAcceptsGzip_HeaderForms — direct unit test of the parser, covering whitespace-heavy forms (" gzip ; q = 0.5 "), GZip;Q=1, q=0., gzipped, identity;q=0, and a bare ,.

Both tests were confirmed to fail on the pre-fix code (6 of the 14 handler-chain cases failed) and pass after it.

Full suite: go build ./..., go vet ./..., go test ./... -race -cover, and golangci-lint run (0 issues) all clean locally. internal/httpapi coverage is 95.7%.

✅ Checklist

General

  • I have added/updated tests for my changes (go test ./... -race -cover passes locally).
  • go vet ./... and golangci-lint run are clean.
  • I have tested my changes.
  • I have read the CONTRIBUTING documentation and followed the project's code style guidelines.
  • I have updated ARCHITECTURE.md if this changes a documented design decision. — n/a, no documented design decision changes.

REST API / configuration / packaging

  • I have updated docs/API.md to reflect a REST API change. — added a note under "Compression" that q-values are honoured, * counts as accepting gzip, and x-gzip does not.
  • No breaking change to /api/v1/... response shapes, or a new API version (/api/v2/...) was introduced instead.
  • I have updated README.md / packaging/pimonitor.example.yaml — n/a, no configuration change.
  • I have updated packaging/install.sh or the systemd units — n/a, no packaging change.

⏭ Next Steps

None. Accept-Encoding is the only q-value header PiMonitor negotiates on; there is no Accept or Accept-Language handling with the same shape of bug.

withGzip decided whether to compress with strings.Contains(header,
"gzip"), but Accept-Encoding is a q-value list (RFC 9110 12.5.3), not an
opaque string. A client sending "gzip;q=0" is explicitly refusing gzip,
yet the substring match compressed anyway and handed it a body it said it
could not decode. The deprecated "x-gzip" token matched for the same
reason, as would any future coding whose name contains "gzip".

Parse the header instead: split on commas, compare the coding token
exactly (case-insensitively), and skip entries carrying q=0. A bare "*"
now counts as accepting gzip, which is what the header means and what
most servers do; previously "Accept-Encoding: *" got an identity
response. An explicit gzip entry outranks a wildcard, so "gzip;q=0, *"
stays a refusal. A qvalue that fails to parse is ignored rather than read
as a refusal, keeping the prior behaviour for malformed headers.

The JSON response shape is unchanged, so this is not an /api/v1 breaking
change.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit 39a56ee into main Aug 23, 2026
5 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-101-0ofd4t branch August 23, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gzip middleware ignores Accept-Encoding q-values and matches x-gzip

1 participant