Conversation
String.trim_trailing/1 also strips Unicode whitespace, so values such as "5" followed by VT, FF, NBSP, NEL or U+3000 were accepted. RFC 9110 section 8.6 allows only digits, with optional whitespace around the field value.
Folded header and trailer values were delivered with the CRLF and leading whitespace intact. RFC 9112 section 5.2 requires user agents to replace each obs-fold with a space before interpreting the value. With :stream_headers a header is emitted before its continuation line can be seen, so folds are rejected with :invalid_header in that mode.
:erlang.decode_packet/3 accepts any major version, status codes with two or four digits, and reason phrases containing control bytes. Only HTTP/1.x with a single-digit minor version, three-digit status codes, and reason phrases made of HTAB, SP, VCHAR and obs-text are accepted.
Values with CR, NUL, DEL or other control bytes were delivered verbatim in headers and trailers. RFC 9110 5.5 allows only HTAB, SP, VCHAR and obs-text in a field value and requires recipients to reject or replace CR, LF and NUL. The response now fails with :invalid_header or :invalid_trailer_header, the same check the request encoder applies to outgoing values.
:erlang.decode_packet/3 accepts a line such as ": bar" and returns an
empty name, which was delivered to callers as {"", "bar"}. RFC 9110 5.1
defines field-name as a token of at least one character.
RFC 9112 5.1 excludes optional whitespace after a field value from the value itself. :erlang.decode_packet/3 strips the leading whitespace but keeps the trailing one, so values such as "bar " reached callers with the whitespace and only Content-Length trimmed it. Trim every header and trailer value in one place and make the Content-Length parser accept digits only.
An obs-fold at the start of a header value ("Foo:" followed by a
continuation line) was replaced by a space that stayed in the delivered
value. RFC 9112 5.1 excludes the optional whitespace around a value, so
leading whitespace is now trimmed like trailing whitespace already was.
With the :stream_headers option a folded value was rejected even when
the whole header section arrived in one message, while the default mode
replaced the fold. The replacement now applies in both modes; a
continuation line that arrives after its header was emitted is still
rejected.
Coverage Report for CI Build 3Coverage increased (+0.5%) to 89.226%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
馃挍 - Coveralls |
:erlang.decode_packet/3 returns the version and status code as integers, so "HTTP/01.1", "HTTP/1.01" and a status code of "0200" were accepted as HTTP/1.1 and 200. RFC 9112 defines the version as "HTTP/" DIGIT "." DIGIT and the status code as three digits, so the raw status line is checked as well.
RFC 9112 5.2 defines obs-fold as OWS CRLF RWS, but only the line break and the whitespace after it were replaced, so "one\t\r\n two" was delivered as "one\t two" instead of "one two".
decode_packet/3 accepts status lines that RFC 9112 doesn't, and returns the version and status code as integers, so the raw line had to be checked again after it. The status line is now matched directly against the RFC 9112 grammar in one pass. The only difference in accepted lines is that the reason phrase keeps whitespace after the separating space, as the grammar requires, instead of having it stripped.
ericmj
marked this pull request as ready for review
September 24, 2026 11:57
Splitting the line on LF with :binary.split/2 and validating the reason phrase afterwards was slower than :erlang.decode_packet/3 with the same validation. The reason phrase is now validated while looking for the end of the line.
Header values were decoded with decode_packet/3 and then scanned again to replace obs-folds, trim whitespace and validate the name and value bytes. Header lines are now parsed in one pass that reads 7 bytes at a time as an integer and uses bit tricks to find the colon and the line end while validating the name and value. Bare LF line endings and obs-folds are handled by the parser, so the sentinel byte :stream_headers appended to make decode_packet/3 emit a header at the end of the data isn't needed. Co-authored-by: Wojtek Mach <wojtekmach@users.noreply.github.com>
ericmj
marked this pull request as draft
September 24, 2026 12:09
ericmj
marked this pull request as ready for review
September 24, 2026 12:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes to how
Mint.HTTP1parses the status line and header fields. Each commit is one fix with its own tests, so this is meant to be rebase-merged rather than squashed.String.trim_trailing/1also stripped Unicode whitespace such as VT, FF or NBSP (RFC 9110 搂8.6).:erlang.decode_packet/3accepts any major version, two- or four-digit status codes and control bytes in the reason phrase; only HTTP/1.x with a single-digit minor version, three-digit codes and reason phrases of HTAB, SP, VCHAR and obs-text are accepted now.:erlang.decode_packet/3, which also accepted leading zeros (HTTP/01.1,HTTP/1.01,0200) that it returns as plain integers. Whitespace after the separating space now stays in the reason phrase, as the grammar requires.:invalid_header/:invalid_trailer_header, the same check the request encoder applies to outgoing values (RFC 9110 搂5.5).{"", value}(RFC 9110 搂5.1).:stream_headerstoo when the continuation line arrives together with its header. A continuation line that arrives after its header was emitted is still rejected. Whitespace before the line break is replaced along with it, since obs-fold isOWS CRLF RWS. The:stream_headersdocs now describe this.:erlang.decode_packet/3followed by separate scans for obs-folds, whitespace and invalid bytes. The parser reads 7 bytes at a time as an integer and uses bit tricks (SWAR) to find the colon and the line end while validating the name and value. It handles bare LF line endings and obs-folds, and removes the sentinel byte:stream_headersneeded to makedecode_packet/3emit a header at the end of the data.Benchmarks, compared with the same validation done after
:erlang.decode_packet/3. Each figure is the median of five shuffled rounds, each timing many calls in a fresh process after a warm-up (Elixir 1.20.4, OTP 29, Apple M5 Max).Status line parsing, 2M calls per round after 200k warm-up calls:
decode_packet+ checksHTTP/1.1 200 OK(17 B)HTTP/1.1 404 Not Found(24 B)The reason phrase is validated one byte at a time, so the native version is faster on unusually long reason phrases.
Header block parsing, one header line at a time until the end of the section:
decode_packet+ checksThe SWAR header parser is by @wojtekmach.
Every commit's tests were run without its fix applied and fail there.