Skip to content

Fix HTTP/1 Transfer-Encoding, 1xx close and upgrade handling - #521

Open
ericmj wants to merge 5 commits into
mainfrom
http1-followups
Open

ericmj wants to merge 5 commits into
mainfrom
http1-followups

Conversation

@ericmj

@ericmj ericmj commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Five fixes to HTTP/1 response handling, one commit each, meant to be rebase-merged rather than squashed.

  • Transfer-Encoding codings with parameters, such as custom; level=1, chunked, are accepted (RFC 9110 10.1.4). Chunked with parameters is still an error (RFC 9112 7.1). Multiple Transfer-Encoding fields are combined before parsing. Request headers passed with a streamed body and no Content-Length go through the same parser, and chunked is appended after the last coding unless chunked or identity is already there.
  • A Transfer-Encoding field with no codings is no longer an error. It still counts as present for the Content-Length conflict and the HTTP/1.0 close. Transfer-Encoding values are only parsed when they decide the framing, so a malformed one no longer fails a response to HEAD, a 204 or 304, or a 2xx to CONNECT. The commit message lists the resulting changes in which error a malformed value produces.
  • A Connection: close in a 1xx response closes the connection after the final response, as if the final response carried it. Before, it was dropped when the 1xx was reset. Queued requests fail with :unprocessed, also when the final response is a 101.
  • The comment on message_body/1 cites RFC 9112 6.3 instead of RFC 7230.
  • Requests pipelined behind a 101 response fail with a new :connection_upgraded error, since later bytes belong to the new protocol. :unprocessed isn't used because the server may have received the request bytes as part of that protocol.

@coveralls

coveralls commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 0

Coverage increased (+0.04%) to 90.547%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (50 of 51 lines covered, 98.04%).
  • 5 coverage regressions across 3 files.

Uncovered Changes

File Changed Covered %
lib/mint/http1.ex 24 23 95.83%
Total (2 files) 51 50 98.04%

Coverage Regressions

5 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
lib/mint/core/headers.ex 3 78.95%
lib/mint/http1.ex 1 87.56%
lib/mint/http1/response.ex 1 96.05%

Coverage Stats

Coverage Status
Relevant Lines: 2010
Covered Lines: 1820
Line Coverage: 90.55%
Coverage Strength: 707.11 hits per line

馃挍 - Coveralls

A Transfer-Encoding value with a parameter, such as
"custom; level=1, chunked", failed with {:invalid_token_list, value}
because the value was parsed as a plain list of tokens. RFC 9110 10.1.4
allows parameters on a transfer coding, as a token or a quoted string.

Transfer-Encoding values are now parsed with their parameters and only
the coding names are kept, so the final coding of a response still
decides between chunked framing and reading until close. The chunked
coding defines no parameters and RFC 9112 7.1 says their presence
should be treated as an error, so chunked with parameters is still
rejected.

The same parser checks the Transfer-Encoding headers passed to request/5
with a streamed body and no Content-Length, so a value such as
"gzip;q=1" is accepted there. The fields are combined with ", " into
one value before parsing (RFC 9110 5.3). If chunked or identity is among
the codings the headers are sent unchanged, as before. Otherwise chunked
is appended to the last field, so it ends up as the final coding once.
A Transfer-Encoding field with no list elements, such as an empty value
or ",", failed the response with :empty_token_list and closed the
connection. For a response to HEAD this happened before the rule that
such a response has no body, and a comma-only field between two
nonempty Transfer-Encoding fields failed the response instead of being
ignored.

RFC 9110 5.6.1 allows a #transfer-coding list with zero elements, so
such a field now adds no transfer coding when the framing is chosen. A
response with only an empty Transfer-Encoding and no Content-Length has
no framing, so its body is read until the connection closes (RFC 9112
6.3). The field still counts as present: with Content-Length it's an
error, and it makes an HTTP/1.0 response, including a 1xx one, close
the connection (RFC 9112 6.1).

Transfer-Encoding values are parsed only where they decide the framing,
combined into one value as on the request side, so a malformed value no
longer fails a response to HEAD, a 204 or 304 response, or a 2xx
response to CONNECT, whose Transfer-Encoding RFC 9110 9.3.6 says to
ignore. Where it does fail, the error comes after the :headers event
(with :stream_headers the field itself can be emitted first) and not
while the header section is incomplete, other header errors in the same
section are reported instead of it, and with Content-Length the error is
:transfer_encoding_and_content_length instead of {:invalid_token_list,
value}.

On the request side, a Transfer-Encoding field with no codings passed to
request/5 with a streamed body counts as no codings: when chunked is
appended to such a last field it replaces the empty value.
The response fields of a request were reset after an informational
response so the final response could be parsed, and that dropped the
Connection options of the 1xx response. A "close" option in a 1xx
response was ignored, and the connection stayed open for the requests
pipelined behind it.

RFC 9112 9.6 has a client that receives "close" close the connection
after reading the response containing it. A 1xx isn't the complete
response, so Mint now keeps the option and closes the connection after
the final response, handling it as if the final response carried it:
the connection is closed after the final response, including a 101 or
an HTTP/1.0 keep-alive one, and queued requests fail with :unprocessed.
A 2xx response to CONNECT still keeps the connection open for the
tunnel. Other Connection options of a 1xx response, such as keep-alive,
don't apply to the final response.
The comment on message_body/1 quoted RFC 7230 3.3.3, which RFC 9112 6.3
replaces. It now summarises the RFC 9112 6.3 rules the function applies,
starting with the responses that have no body, and the comment on the
Transfer-Encoding clause uses the same section's wording.
After a 101 response the connection stayed open with the queued
requests in place, so bytes arriving later, which belong to the protocol
the server switched to (RFC 9110 15.2.2), were parsed as HTTP/1
responses to those requests.

Queued requests now fail with the new :connection_upgraded error when
the 101 response completes. Bytes arriving later are unexpected data as
long as no new request is made on the connection. A failed request that
was streaming its body can't stream the rest of it. :unprocessed isn't
used because the pipelined request bytes reached the server as part of
the new protocol, so the request might have been processed. The bytes
after the 101 response in the same packet are still delivered as data
for the upgrade request.
@ericmj
ericmj marked this pull request as ready for review September 28, 2026 16:23
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.

2 participants