diff --git a/lib/mint/http1.ex b/lib/mint/http1.ex index be31db7c..c2154d53 100644 --- a/lib/mint/http1.ex +++ b/lib/mint/http1.ex @@ -742,6 +742,27 @@ defmodule Mint.HTTP1 do {:ok, conn, responses} end + # Informational (1xx) responses have no body and must not finalize the + # request; the final response follows on the same request ref. Reset the + # request's response-side fields and continue parsing without popping it. + defp decode_body(:informational, conn, data, _request_ref, responses) do + request = %{ + conn.request + | state: :status, + version: nil, + status: nil, + headers_buffer: [], + data_buffer: [], + content_length: nil, + connection: [], + transfer_encoding: [], + body: nil + } + + conn = %{conn | request: request, buffer: ""} + decode(:status, conn, data, responses) + end + defp decode_body(:single, conn, data, request_ref, responses) do {conn, responses} = add_body(conn, data, responses) conn = request_done(conn) @@ -981,7 +1002,10 @@ defmodule Mint.HTTP1 do status == 101 -> {:ok, :single} - method == "HEAD" or status in 100..199 or status in [204, 304] -> + status in 100..199 -> + {:ok, :informational} + + method == "HEAD" or status in [204, 304] -> {:ok, :none} # method == "CONNECT" and status in 200..299 -> nil diff --git a/test/mint/http1/conn_test.exs b/test/mint/http1/conn_test.exs index 66f39876..e354be14 100644 --- a/test/mint/http1/conn_test.exs +++ b/test/mint/http1/conn_test.exs @@ -327,6 +327,84 @@ defmodule Mint.HTTP1Test do assert conn.buffer == "XXX" end + test "100 Continue informational response followed by final response", %{conn: conn} do + {:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "hello") + + response = + "HTTP/1.1 100 Continue\r\n\r\n" <> + "HTTP/1.1 201 Created\r\ncontent-length: 2\r\n\r\nok" + + assert {:ok, conn, + [ + {:status, ^ref, 100}, + {:headers, ^ref, []}, + {:status, ^ref, 201}, + {:headers, ^ref, [{"content-length", "2"}]}, + {:data, ^ref, "ok"}, + {:done, ^ref} + ]} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + + assert HTTP1.open?(conn) + end + + test "103 Early Hints informational response followed by final response", %{conn: conn} do + {:ok, conn, ref} = HTTP1.request(conn, "GET", "/", [], nil) + + response = + "HTTP/1.1 103 Early Hints\r\nlink: ; rel=preload\r\n\r\n" <> + "HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok" + + assert {:ok, _conn, + [ + {:status, ^ref, 103}, + {:headers, ^ref, [{"link", "; rel=preload"}]}, + {:status, ^ref, 200}, + {:headers, ^ref, [{"content-length", "2"}]}, + {:data, ^ref, "ok"}, + {:done, ^ref} + ]} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + end + + test "multiple informational responses before final response", %{conn: conn} do + {:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "x") + + response = + "HTTP/1.1 100 Continue\r\n\r\n" <> + "HTTP/1.1 103 Early Hints\r\nlink: ; rel=preload\r\n\r\n" <> + "HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok" + + assert {:ok, _conn, + [ + {:status, ^ref, 100}, + {:headers, ^ref, []}, + {:status, ^ref, 103}, + {:headers, ^ref, [{"link", "; rel=preload"}]}, + {:status, ^ref, 200}, + {:headers, ^ref, [{"content-length", "2"}]}, + {:data, ^ref, "ok"}, + {:done, ^ref} + ]} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + end + + test "informational response split across multiple TCP messages", %{conn: conn} do + {:ok, conn, ref} = HTTP1.request(conn, "POST", "/", [{"expect", "100-continue"}], "x") + + assert {:ok, conn, [{:status, ^ref, 100}, {:headers, ^ref, []}]} = + HTTP1.stream(conn, {:tcp, conn.socket, "HTTP/1.1 100 Continue\r\n\r\n"}) + + assert {:ok, _conn, + [ + {:status, ^ref, 201}, + {:headers, ^ref, [{"content-length", "2"}]}, + {:data, ^ref, "ok"}, + {:done, ^ref} + ]} = + HTTP1.stream( + conn, + {:tcp, conn.socket, "HTTP/1.1 201 Created\r\ncontent-length: 2\r\n\r\nok"} + ) + end + test "body following a 101 switching-protocols", %{conn: conn} do {:ok, conn, ref} = HTTP1.request(conn, "GET", "/socket/websocket", [], nil)