Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/bandit/http1/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ defmodule Bandit.HTTP1.Socket do
resp_line = "#{socket.version} #{status} #{Plug.Conn.Status.reason_phrase(status)}\r\n"

{headers, socket} = handle_keepalive(status, headers, socket)
headers = remove_http_1_0_transfer_encoding(headers, socket)

has_content_length = Bandit.Headers.get_header(headers, "content-length") != nil

Expand All @@ -465,6 +466,15 @@ defmodule Bandit.HTTP1.Socket do
# call. This makes a _substantial_ difference in practice
%{socket | write_state: :writing, send_buffer: [resp_line | encode_headers(headers)]}

:chunk_encoded when not has_content_length and socket.version == :"HTTP/1.0" ->
# HTTP/1.0 has no chunked transfer coding. Delimit the response body by closing the
# connection, even if the client requested a persistent connection.
headers =
[{"connection", "close"} | Enum.reject(headers, &(elem(&1, 0) == "connection"))]

send!(socket.socket, [resp_line | encode_headers(headers)])
%{socket | write_state: :chunk_streaming, keepalive: false}

:chunk_encoded when not has_content_length ->
headers = [{"transfer-encoding", "chunked"} | headers]
send!(socket.socket, [resp_line | encode_headers(headers)])
Expand All @@ -484,6 +494,14 @@ defmodule Bandit.HTTP1.Socket do
end
end

# RFC9112§6.1 prohibits Transfer-Encoding in every response to an HTTP/1.0 request,
# including a field supplied explicitly by the application.
defp remove_http_1_0_transfer_encoding(headers, %@for{version: :"HTTP/1.0"}) do
Enum.reject(headers, &(elem(&1, 0) == "transfer-encoding"))
end

defp remove_http_1_0_transfer_encoding(headers, _socket), do: headers

defp handle_keepalive(status, headers, socket) do
response_connection_header = safe_downcase(Bandit.Headers.get_header(headers, "connection"))

Expand Down
51 changes: 51 additions & 0 deletions test/bandit/http1/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,6 +1551,57 @@ defmodule HTTP1ProtocolTest do
conn
end

test "uses close-delimited streaming for an HTTP/1.0 response", context do
client = SimpleHTTP1Client.tcp_client(context)

SimpleHTTP1Client.send(
client,
"GET",
"/send_chunked_200",
["host: localhost", "connection: keep-alive"],
"1.0"
)

response = recv_until_closed(client)
assert {:ok, "200 OK", headers, "OK"} = SimpleHTTP1Client.parse_response(client, response)

refute Bandit.Headers.get_header(headers, :"transfer-encoding")
assert Bandit.Headers.get_header(headers, :connection) == "close"
end

defp recv_until_closed(client, response \\ "") do
case Transport.recv(client, 0) do
{:ok, data} -> recv_until_closed(client, response <> data)
{:error, :closed} -> response
end
end

test "removes an application-supplied transfer-encoding from HTTP/1.0", context do
client = SimpleHTTP1Client.tcp_client(context)

SimpleHTTP1Client.send(
client,
"GET",
"/send_chunked_200_with_transfer_encoding",
["host: localhost"],
"1.0"
)

response = recv_until_closed(client)
assert {:ok, "200 OK", headers, "OK"} = SimpleHTTP1Client.parse_response(client, response)
refute Bandit.Headers.get_header(headers, :"transfer-encoding")
end

def send_chunked_200_with_transfer_encoding(conn) do
{:ok, conn} =
conn
|> put_resp_header("transfer-encoding", "chunked")
|> send_chunked(200)
|> chunk("OK")

conn
end

test "streams a content-length delimited response if content-length is set before chunking",
context do
response = Req.get!(context.req, url: "/send_chunked_200_with_content_length")
Expand Down
Loading