Skip to content

Send the final response to a HEAD request after an informational response - #748

Merged
mtrudel merged 1 commit into
mtrudel:mainfrom
neilberkman:head-informational-response
Nov 11, 2026
Merged

mtrudel merged 1 commit into
mtrudel:mainfrom
neilberkman:head-informational-response

Conversation

@neilberkman

Copy link
Copy Markdown
Contributor

An informational response to a HEAD request broke the final response. Bandit.Adapter.send_headers/4 turns the body disposition into :no_body whenever the response has no body to send, which is always the case for HEAD, and it did so for :inform too. The interim response was therefore sent as if it were the complete response:

  • over HTTP/1 the socket was left in the :sent write state, the final response raised a FunctionClauseError in send_headers/4, and the client received a 500;
  • over HTTP/2 the interim HEADERS frame carried END_STREAM, which makes the response malformed (RFC9113§8.1), and the final response failed against the closed stream.

Both the automatic 100 Continue sent when a plug reads a body that came with expect: 100-continue and anything sent with Plug.Conn.inform/3 take this path. The same requests with GET work.

This change leaves an :inform disposition alone and applies the no-body rule only to the final response. An informational response never has a body whatever the request method (RFC9110§15.2).

Four tests, next to the existing informational and 100 Continue tests in test/bandit/http1/protocol_test.exs and test/bandit/http2/plug_test.exs: HEAD with inform/3 and HEAD with expect: 100-continue on each protocol, asserting the interim response followed by the plug's status with its headers and no body.

Verified with mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict, mix dialyzer, and mix test on Elixir 1.20.4 / OTP 29 (1078 passed, 1 skipped, 8 excluded; the 3 failures are in HibernationTest and also fail on main under OTP 29).

…onse

A response to a HEAD request is sent without a body by turning the body
disposition of its headers into :no_body. This happened for
informational responses too, so an interim response to a HEAD request
(the 100 Continue sent when the plug reads a body sent with
`expect: 100-continue`, or anything sent with inform/3) was sent as if
it were the complete response. Over HTTP/1 the socket was left in the
:sent write state and the final response raised a FunctionClauseError,
which went out as a 500. Over HTTP/2 the interim HEADERS frame carried
END_STREAM, which makes it malformed (RFC9113§8.1), and the final
response failed against the closed stream in the same way.

An informational response never has a body whatever the request
method, so leave its disposition alone and apply the HEAD rule only to
the final response.
@mtrudel
mtrudel merged commit 2a611b3 into mtrudel:main Nov 11, 2026
24 of 29 checks passed
@mtrudel

mtrudel commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Ooof. good catch!

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