diff --git a/lib/mint/http1.ex b/lib/mint/http1.ex index 8effc32d..81a9baa9 100644 --- a/lib/mint/http1.ex +++ b/lib/mint/http1.ex @@ -873,6 +873,8 @@ defmodule Mint.HTTP1 do # 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. + # A "close" option is kept, so the connection is closed after the final + # response (RFC 9112 9.6). defp decode_body(:informational, conn, data, _request_ref, responses) do request = %{ conn.request @@ -883,7 +885,7 @@ defmodule Mint.HTTP1 do headers_size: 0, data_buffer: [], content_length: nil, - connection: [], + connection: if("close" in conn.request.connection, do: ["close"], else: []), transfer_encoding: nil, body: nil } diff --git a/test/mint/http1/conn_test.exs b/test/mint/http1/conn_test.exs index 3d6837d4..3b3414dd 100644 --- a/test/mint/http1/conn_test.exs +++ b/test/mint/http1/conn_test.exs @@ -1148,6 +1148,78 @@ defmodule Mint.HTTP1Test do ) end + for {name, final_response} <- [ + {"a final", "HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n"}, + {"a 101", "HTTP/1.1 101 Switching Protocols\r\nupgrade: foo\r\n\r\n"}, + {"an HTTP/1.0 keep-alive", "HTTP/1.0 200 OK\r\nconnection: keep-alive\r\n\r\n"} + ] do + test "connection: close in an informational response closes after #{name} response", + %{conn: conn} do + {:ok, conn, ref1} = HTTP1.request(conn, "HEAD", "/", [], nil) + {:ok, conn, ref2} = HTTP1.request(conn, "GET", "/", [], nil) + + response = + "HTTP/1.1 103 Early Hints\r\nconnection: close\r\n\r\n" <> unquote(final_response) + + assert {:ok, conn, responses} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + + assert [ + {:status, ^ref1, 103}, + {:headers, ^ref1, [{"connection", "close"}]}, + {:status, ^ref1, _}, + {:headers, ^ref1, _}, + {:done, ^ref1}, + {:error, ^ref2, %HTTPError{reason: :unprocessed}} + ] = responses + + refute HTTP1.open?(conn) + end + end + + test "connection: close in an informational response is ignored after a 2xx response to CONNECT", + %{conn: conn} do + {:ok, conn, ref} = HTTP1.request(conn, "CONNECT", "example.com:443", [], nil) + + response = + "HTTP/1.1 103 Early Hints\r\nconnection: close\r\n\r\n" <> + "HTTP/1.1 200 Connection established\r\n\r\n" + + assert {:ok, conn, + [ + {:status, ^ref, 103}, + {:headers, ^ref, _}, + {:status, ^ref, 200}, + {:headers, ^ref, []}, + {:done, ^ref} + ]} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + + assert HTTP1.open?(conn) + end + + test "connection: keep-alive in an informational response doesn't apply to the final response", + %{conn: conn} do + {:ok, conn, ref1} = HTTP1.request(conn, "GET", "/", [], nil) + {:ok, conn, ref2} = HTTP1.request(conn, "GET", "/", [], nil) + + response = + "HTTP/1.1 103 Early Hints\r\nconnection: keep-alive\r\n\r\n" <> + "HTTP/1.0 200 OK\r\ncontent-length: 2\r\n\r\nok" + + assert {:ok, conn, responses} = HTTP1.stream(conn, {:tcp, conn.socket, response}) + + assert [ + {:status, ^ref1, 103}, + {:headers, ^ref1, _}, + {:status, ^ref1, 200}, + {:headers, ^ref1, _}, + {:data, ^ref1, "ok"}, + {:done, ^ref1}, + {:error, ^ref2, %TransportError{reason: :closed}} + ] = responses + + refute HTTP1.open?(conn) + end + test "body following a 101 switching-protocols", %{conn: conn} do {:ok, conn, ref} = HTTP1.request(conn, "GET", "/socket/websocket", [], nil)