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
16 changes: 14 additions & 2 deletions lib/bandit/http1/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Bandit.HTTP1.Socket do
@type read_state :: :unread | :headers_read | :read

@typedoc "An HTTP/1 write state"
@type write_state :: :unsent | :writing | :chunking | :sent
@type write_state :: :unsent | :writing | :chunking | :chunk_streaming | :sent

@typedoc "The information necessary to communicate to/from a socket"
@type t :: %__MODULE__{
Expand Down Expand Up @@ -328,18 +328,24 @@ defmodule Bandit.HTTP1.Socket do

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

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

case body_disposition do
:raw ->
# This is an optimization for the common case of sending a non-encoded body (or file),
# and coalesces the header and body send calls into a single ThousandIsland.Socket.send/2
# call. This makes a _substantial_ difference in practice
%{socket | write_state: :writing, send_buffer: [resp_line | encode_headers(headers)]}

:chunk_encoded ->
:chunk_encoded when not has_content_length ->
headers = [{"transfer-encoding", "chunked"} | headers]
send!(socket.socket, [resp_line | encode_headers(headers)])
%{socket | write_state: :chunking}

:chunk_encoded when has_content_length ->
send!(socket.socket, [resp_line | encode_headers(headers)])
%{socket | write_state: :chunk_streaming}

:no_body ->
send!(socket.socket, [resp_line | encode_headers(headers)])
%{socket | write_state: :sent}
Expand Down Expand Up @@ -394,6 +400,12 @@ defmodule Bandit.HTTP1.Socket do
%{socket | write_state: write_state}
end

def send_data(%@for{write_state: :chunk_streaming} = socket, data, end_request) do
send!(socket.socket, data)
write_state = if end_request, do: :sent, else: :chunk_streaming
%{socket | write_state: write_state}
end

def sendfile(%@for{write_state: :writing} = socket, path, offset, length) do
send!(socket.socket, socket.send_buffer)

Expand Down
22 changes: 22 additions & 0 deletions test/bandit/http1/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,28 @@ defmodule HTTP1ProtocolTest do
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")

assert response.status == 200
assert response.body == "OK"
assert response.headers["transfer-encoding"] != ["chunked"]
assert response.headers["content-length"] == ["2"]
end

def send_chunked_200_with_content_length(conn) do
conn =
conn
|> put_resp_header("content-length", "2")
|> send_chunked(200)

{:ok, conn} = chunk(conn, "O")
{:ok, conn} = chunk(conn, "K")

conn
end

test "does not add the transfer-encoding header for 204 responses", context do
response = Req.get!(context.req, url: "/send_chunked_204")

Expand Down
9 changes: 8 additions & 1 deletion test/bandit/http2/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,14 @@ defmodule HTTP2ProtocolTest do

SimpleH2Client.send_simple_headers(socket, 1, :get, "/chunk_response", context.port)

assert SimpleH2Client.successful_response?(socket, 1, false)
assert {:ok, 1, false,
[
{":status", "200"},
{"date", _date},
{"vary", "accept-encoding"},
{"cache-control", "max-age=0, private, must-revalidate"}
], _ctx} = SimpleH2Client.recv_headers(socket)

assert SimpleH2Client.recv_body(socket) == {:ok, 1, false, "OK"}
assert SimpleH2Client.recv_body(socket) == {:ok, 1, false, "DOKEE"}
assert SimpleH2Client.recv_body(socket) == {:ok, 1, true, ""}
Expand Down