Skip to content

Commit e7aac6e

Browse files
committed
Add support for streaming responses if content-length is set when chunking
1 parent e3f29e8 commit e7aac6e

3 files changed

Lines changed: 42 additions & 3 deletions

File tree

lib/bandit/http1/socket.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule Bandit.HTTP1.Socket do
2424
@type read_state :: :unread | :headers_read | :read
2525

2626
@typedoc "An HTTP/1 write state"
27-
@type write_state :: :unsent | :writing | :chunking | :sent
27+
@type write_state :: :unsent | :writing | :chunking | :chunk_streaming | :sent
2828

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

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

331+
has_content_length = Bandit.Headers.get_header(headers, "content-length") != nil
332+
331333
case body_disposition do
332334
:raw ->
333335
# This is an optimization for the common case of sending a non-encoded body (or file),
334336
# and coalesces the header and body send calls into a single ThousandIsland.Socket.send/2
335337
# call. This makes a _substantial_ difference in practice
336338
%{socket | write_state: :writing, send_buffer: [resp_line | encode_headers(headers)]}
337339

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

345+
:chunk_encoded when has_content_length ->
346+
send!(socket.socket, [resp_line | encode_headers(headers)])
347+
%{socket | write_state: :chunk_streaming}
348+
343349
:no_body ->
344350
send!(socket.socket, [resp_line | encode_headers(headers)])
345351
%{socket | write_state: :sent}
@@ -394,6 +400,12 @@ defmodule Bandit.HTTP1.Socket do
394400
%{socket | write_state: write_state}
395401
end
396402

403+
def send_data(%@for{write_state: :chunk_streaming} = socket, data, end_request) do
404+
send!(socket.socket, data)
405+
write_state = if end_request, do: :sent, else: :chunk_streaming
406+
%{socket | write_state: write_state}
407+
end
408+
397409
def sendfile(%@for{write_state: :writing} = socket, path, offset, length) do
398410
send!(socket.socket, socket.send_buffer)
399411

test/bandit/http1/protocol_test.exs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,26 @@ defmodule HTTP1ProtocolTest do
15641564
conn
15651565
end
15661566

1567+
test "streams a content-length delimited response if content-length is set before chunking",
1568+
context do
1569+
response = Req.get!(context.req, url: "/send_chunked_200_with_content_length")
1570+
1571+
assert response.status == 200
1572+
assert response.body == "OK"
1573+
assert response.headers["transfer-encoding"] != ["chunked"]
1574+
assert response.headers["content-length"] == ["2"]
1575+
end
1576+
1577+
def send_chunked_200_with_content_length(conn) do
1578+
{:ok, conn} =
1579+
conn
1580+
|> put_resp_header("content-length", "2")
1581+
|> send_chunked(200)
1582+
|> chunk("OK")
1583+
1584+
conn
1585+
end
1586+
15671587
test "does not add the transfer-encoding header for 204 responses", context do
15681588
response = Req.get!(context.req, url: "/send_chunked_204")
15691589

test/bandit/http2/protocol_test.exs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,14 @@ defmodule HTTP2ProtocolTest do
880880

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

883-
assert SimpleH2Client.successful_response?(socket, 1, false)
883+
assert {:ok, 1, false,
884+
[
885+
{":status", "200"},
886+
{"date", _date},
887+
{"vary", "accept-encoding"},
888+
{"cache-control", "max-age=0, private, must-revalidate"}
889+
], _ctx} = SimpleH2Client.recv_headers(socket)
890+
884891
assert SimpleH2Client.recv_body(socket) == {:ok, 1, false, "OK"}
885892
assert SimpleH2Client.recv_body(socket) == {:ok, 1, false, "DOKEE"}
886893
assert SimpleH2Client.recv_body(socket) == {:ok, 1, true, ""}

0 commit comments

Comments
 (0)