Skip to content

Validate response headers and protocol versions. - #74

Merged
gustavofreze merged 5 commits into
mainfrom
fix/header-field-validation
Nov 7, 2026
Merged

gustavofreze merged 5 commits into
mainfrom
fix/header-field-validation

Conversation

@gustavofreze

@gustavofreze gustavofreze commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Please follow the contributing guidelines.

Summary

  • Headers::fromArray() (and every path through it: from(), fromMessage(), with(), mergedWith()) rejects a header name that ends in a line feed. The token pattern ended in $ without the D modifier, so "X-Trace\n" matched.
  • The name and value rules live once, in the internal HeaderName and HeaderValue. Headers uses them directly, and the response uses them through the internal ResponseField, the single validation point for every header that reaches a response.
  • A response validates every header that reaches it: the Headerable arguments of the Response factories, withHeader() and withAddedHeader(). A name that is not an RFC 9110 token raises HeaderNameIsInvalid, and a value with a control character other than horizontal tab (CR, LF and NUL included, per RFC 9110 section 5.5, RFC 9113 section 8.2.1 and RFC 9114 section 4.2) raises HeaderValueIsInvalid. Both extend InvalidArgumentException, which is what PSR-7 asks of withHeader() and withAddedHeader().
  • The same paths reject the connection-specific headers with the new HeaderIsConnectionSpecific: Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade, and TE with any value other than trailers (names compared case-insensitively, the TE value trimmed and compared case-insensitively). HTTP/2 (RFC 9113 section 8.2.2) and HTTP/3 (RFC 9114 section 4.2) treat a message carrying one as malformed. The rule applies to responses only: Headers also wraps incoming requests (Request::headers()) and outgoing client requests, where an HTTP/1.1 Connection: keep-alive is ordinary, so rejecting it there would break every request a server receives over HTTP/1.1.
  • The response protocol version is a closed set, the internal ProtocolVersion enum: 1.0, 1.1, 2, 3. withProtocolVersion() stores 2.0 and 3.0 as 2 and 3, and raises the new ProtocolVersionIsUnsupported for anything else. A response still starts on 1.1, the PSR-7 convention.
  • Mirroring the request's protocol version: the Response factories are static and never see the request, and the library ships no middleware that sits where both are in reach, so adding one only for this would be new surface. The README documents the path instead, ->withProtocolVersion($request->getProtocolVersion()), which the closed set makes safe because a 2.0 from SERVER_PROTOCOL is normalized and a malformed value is refused rather than written into the status line.
  • The README documents all of the above under "Creating a response".

Breaking changes and migration

Recommended release: 8.0.0 (major). Each item below is input that used to pass and now throws, so a caller relying on it breaks at runtime.

Used to pass Now Migration
A header name ending in \n in Headers::fromArray(), from(), fromMessage(), with() HeaderNameIsInvalid Trim the name. It never formed a valid header.
Any name or value in Response factories (Headerable), withHeader(), withAddedHeader() HeaderNameIsInvalid or HeaderValueIsInvalid for a non-token name or a value with a control character other than tab Send RFC 9110 tokens and values without CR, LF, NUL or other controls.
Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, Upgrade on a response HeaderIsConnectionSpecific Drop the header. The web server or SAPI owns connection management and framing.
TE with a value other than trailers on a response HeaderIsConnectionSpecific Drop it, or send exactly trailers.
withProtocolVersion() with any string, '' included ('' used to mean 1.1) ProtocolVersionIsUnsupported unless 1.0, 1.1, 2, 2.0, 3, 3.0 Pass one of those, or omit the call to keep 1.1.
withProtocolVersion('2.0') / ('3.0') stored as given Stored and reported as 2 / 3 Compare against 2 / 3.

No public signature changes. The new public types are HeaderIsConnectionSpecific and ProtocolVersionIsUnsupported, both InvalidArgumentException and HttpException.

Dependents: tiny-blocks/http-error-handler, http-health-check, http-logging, http-query, skedli/http-authentication and skedli/http-idempotency all require tiny-blocks/http ^7.2, so each needs a release widening that constraint before a service can install 8.0.0.

Related issue

None.

Checklist

  • Tests added or updated.
  • Documentation updated when applicable.
  • make review passes.
  • make tests passes.

@gustavofreze gustavofreze changed the title Validate header names and values on every response path. Validate response headers and protocol versions. Nov 7, 2026
@gustavofreze
gustavofreze merged commit e5d6272 into main Nov 7, 2026
4 checks passed
@gustavofreze
gustavofreze deleted the fix/header-field-validation branch November 7, 2026 11:49
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.

1 participant