Skip to content

Commit 0e474d7

Browse files
committed
Fix HTTP/1 handling of 1xx informational responses
1xx informational responses (100 Continue, 103 Early Hints, etc.) were treated the same as HEAD and 204/304 responses: Mint emitted `{:done, ref}` and popped the request from its queue. The real final response arriving afterwards had no active request and Mint returned `{:unexpected_data, _}`, closing the connection. This broke two common scenarios: * Requests sent with `Expect: 100-continue`, where the server sends 100 Continue before the final response. * Servers or intermediaries emitting unsolicited 1xx (Plug.Conn.inform/3, HAProxy 102 Processing, CDNs sending 103 Early Hints, etc.). Fix: split 1xx out of the `:none` body branch in `message_body/1` into a new `:informational` body kind. In `decode_body/5`, the `:informational` clause resets the request's response-side fields (`version`, `status`, `headers_buffer`, etc.) back to their initial state and continues parsing from `:status` without popping the request. The `{:status, ref, 1xx}` and `{:headers, ref, _}` responses are still emitted to the caller by the existing `:status`/`:headers` decode stages, so informational responses remain visible; only the premature `{:done, ref}` is suppressed.
1 parent 0bfcc86 commit 0e474d7

File tree

2 files changed

+103
-1
lines changed

2 files changed

+103
-1
lines changed

lib/mint/http1.ex

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,27 @@ defmodule Mint.HTTP1 do
742742
{:ok, conn, responses}
743743
end
744744

745+
# Informational (1xx) responses have no body and must not finalize the
746+
# request; the final response follows on the same request ref. Reset the
747+
# request's response-side fields and continue parsing without popping it.
748+
defp decode_body(:informational, conn, data, _request_ref, responses) do
749+
request = %{
750+
conn.request
751+
| state: :status,
752+
version: nil,
753+
status: nil,
754+
headers_buffer: [],
755+
data_buffer: [],
756+
content_length: nil,
757+
connection: [],
758+
transfer_encoding: [],
759+
body: nil
760+
}
761+
762+
conn = %{conn | request: request, buffer: ""}
763+
decode(:status, conn, data, responses)
764+
end
765+
745766
defp decode_body(:single, conn, data, request_ref, responses) do
746767
{conn, responses} = add_body(conn, data, responses)
747768
conn = request_done(conn)
@@ -981,7 +1002,10 @@ defmodule Mint.HTTP1 do
9811002
status == 101 ->
9821003
{:ok, :single}
9831004

984-
method == "HEAD" or status in 100..199 or status in [204, 304] ->
1005+
status in 100..199 ->
1006+
{:ok, :informational}
1007+
1008+
method == "HEAD" or status in [204, 304] ->
9851009
{:ok, :none}
9861010

9871011
# method == "CONNECT" and status in 200..299 -> nil

test/mint/http1/conn_test.exs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,84 @@ defmodule Mint.HTTP1Test do
327327
assert conn.buffer == "XXX"
328328
end
329329

330+
test "100 Continue informational response followed by final response", %{conn: conn} do
331+
{:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "hello")
332+
333+
response =
334+
"HTTP/1.1 100 Continue\r\n\r\n" <>
335+
"HTTP/1.1 201 Created\r\ncontent-length: 2\r\n\r\nok"
336+
337+
assert {:ok, conn,
338+
[
339+
{:status, ^ref, 100},
340+
{:headers, ^ref, []},
341+
{:status, ^ref, 201},
342+
{:headers, ^ref, [{"content-length", "2"}]},
343+
{:data, ^ref, "ok"},
344+
{:done, ^ref}
345+
]} = HTTP1.stream(conn, {:tcp, conn.socket, response})
346+
347+
assert HTTP1.open?(conn)
348+
end
349+
350+
test "103 Early Hints informational response followed by final response", %{conn: conn} do
351+
{:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil)
352+
353+
response =
354+
"HTTP/1.1 103 Early Hints\r\nlink: </style.css>; rel=preload\r\n\r\n" <>
355+
"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok"
356+
357+
assert {:ok, _conn,
358+
[
359+
{:status, ^ref, 103},
360+
{:headers, ^ref, [{"link", "</style.css>; rel=preload"}]},
361+
{:status, ^ref, 200},
362+
{:headers, ^ref, [{"content-length", "2"}]},
363+
{:data, ^ref, "ok"},
364+
{:done, ^ref}
365+
]} = HTTP1.stream(conn, {:tcp, conn.socket, response})
366+
end
367+
368+
test "multiple informational responses before final response", %{conn: conn} do
369+
{:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "x")
370+
371+
response =
372+
"HTTP/1.1 100 Continue\r\n\r\n" <>
373+
"HTTP/1.1 103 Early Hints\r\nlink: </a>; rel=preload\r\n\r\n" <>
374+
"HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok"
375+
376+
assert {:ok, _conn,
377+
[
378+
{:status, ^ref, 100},
379+
{:headers, ^ref, []},
380+
{:status, ^ref, 103},
381+
{:headers, ^ref, [{"link", "</a>; rel=preload"}]},
382+
{:status, ^ref, 200},
383+
{:headers, ^ref, [{"content-length", "2"}]},
384+
{:data, ^ref, "ok"},
385+
{:done, ^ref}
386+
]} = HTTP1.stream(conn, {:tcp, conn.socket, response})
387+
end
388+
389+
test "informational response split across multiple TCP messages", %{conn: conn} do
390+
{:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "x")
391+
392+
assert {:ok, conn, [{:status, ^ref, 100}, {:headers, ^ref, []}]} =
393+
HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 100 Continue\r\n\r\n"})
394+
395+
assert {:ok, _conn,
396+
[
397+
{:status, ^ref, 201},
398+
{:headers, ^ref, [{"content-length", "2"}]},
399+
{:data, ^ref, "ok"},
400+
{:done, ^ref}
401+
]} =
402+
HTTP1.stream(
403+
conn,
404+
{:tcp, conn.socket, "HTTP/1.1 201 Created\r\ncontent-length: 2\r\n\r\nok"}
405+
)
406+
end
407+
330408
test "body following a 101 switching-protocols", %{conn: conn} do
331409
{:ok, conn, ref} = HTTP1.request(conn, "GET", "/socket/websocket", [], nil)
332410

0 commit comments

Comments
 (0)