Skip to content

Commit

Permalink
Handle RST_STREAM messages when writing H2 responses
Browse files Browse the repository at this point in the history
  • Loading branch information
mtrudel committed Dec 2, 2024
1 parent 51131c3 commit e2a8ea7
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 4 deletions.
6 changes: 6 additions & 0 deletions lib/bandit/http2/errors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ defmodule Bandit.HTTP2.Errors do
http_1_1_requires: 0xD
}

@spec to_reason(integer()) :: atom()

for {name, value} <- error_codes do
@spec unquote(name)() :: unquote(Macro.var(name, Elixir)) :: unquote(value)
def unquote(name)(), do: unquote(value)

def to_reason(unquote(value)), do: unquote(name)
end

def to_reason(_), do: :unknown

# Represents a stream error as defined in RFC9113§5.4.2
defmodule StreamError do
defexception [:message, :error_code]
Expand Down
15 changes: 13 additions & 2 deletions lib/bandit/http2/stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,18 @@ defmodule Bandit.HTTP2.Stream do
end

@spec do_recv_rst_stream!(term(), term()) :: no_return()
defp do_recv_rst_stream!(_stream, error_code),
do: raise("Client sent RST_STREAM with error code #{error_code}")
defp do_recv_rst_stream!(_stream, error_code) do
case Bandit.HTTP2.Errors.to_reason(error_code) do
reason when reason in [:no_error, :cancel] ->
raise(Bandit.TransportError, message: "Client reset stream normally", error: :closed)

reason ->
raise(Bandit.TransportError,
message: "Received RST_STREAM from client: #{reason} (#{error_code})",
error: reason
)
end
end

@spec do_stream_closed_error!(term()) :: no_return()
defp do_stream_closed_error!(msg), do: stream_error!(msg, Bandit.HTTP2.Errors.stream_closed())
Expand Down Expand Up @@ -418,6 +428,7 @@ defmodule Bandit.HTTP2.Stream do
stream =
receive do
{:send_window_update, delta} -> do_recv_send_window_update(stream, delta)
{:rst_stream, error_code} -> do_recv_rst_stream!(stream, error_code)
after
0 -> stream
end
Expand Down
57 changes: 55 additions & 2 deletions test/bandit/http2/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1997,7 +1997,7 @@ defmodule HTTP2ProtocolTest do
assert output =~ "(Bandit.HTTP2.Errors.ConnectionError) Received RST_STREAM in idle state"
end

test "raises an error on upon receipt of an RST_STREAM frame", context do
test "raises an error upon receipt of an RST_STREAM frame during reading", context do
socket = SimpleH2Client.setup_connection(context)

errors =
Expand All @@ -2007,12 +2007,65 @@ defmodule HTTP2ProtocolTest do
Process.sleep(100)
end)

assert errors =~ "Client sent RST_STREAM with error code 99"
assert errors =~ "(Bandit.TransportError) Received RST_STREAM from client: unknown (99)"
end

def expect_reset(conn) do
read_body(conn)
end

test "raises an error upon receipt of an RST_STREAM frame during writing", context do
socket = SimpleH2Client.setup_connection(context)

errors =
capture_log(fn ->
SimpleH2Client.send_simple_headers(socket, 1, :get, "/write_after_delay", context.port)
SimpleH2Client.send_rst_stream(socket, 1, 99)
Process.sleep(200)
end)

assert errors =~ "(Bandit.TransportError) Received RST_STREAM from client: unknown (99)"
end

def write_after_delay(conn) do
Process.sleep(100)
send_resp(conn, 200, "OK")
end

test "considers :no_error RST_STREAM frame as a normal closure during chunk writing",
context do
socket = SimpleH2Client.setup_connection(context)

errors =
capture_log(fn ->
SimpleH2Client.send_simple_headers(socket, 1, :get, "/expect_chunk_error", context.port)
SimpleH2Client.send_rst_stream(socket, 1, 0)
Process.sleep(200)
end)

assert errors == ""
end

test "considers :cancel RST_STREAM frame as a normal closure during chunk writing",
context do
socket = SimpleH2Client.setup_connection(context)

errors =
capture_log(fn ->
SimpleH2Client.send_simple_headers(socket, 1, :get, "/expect_chunk_error", context.port)
SimpleH2Client.send_rst_stream(socket, 1, 8)
Process.sleep(200)
end)

assert errors == ""
end

def expect_chunk_error(conn) do
conn = send_chunked(conn, 200)
Process.sleep(100)
{:error, :closed} = chunk(conn, "CHUNK")
conn
end
end

describe "SETTINGS frames" do
Expand Down

0 comments on commit e2a8ea7

Please sign in to comment.