diff --git a/src/gun_http2.erl b/src/gun_http2.erl index 22654ca2..eb40ca10 100644 --- a/src/gun_http2.erl +++ b/src/gun_http2.erl @@ -908,9 +908,10 @@ update_window(State0=#http2_state{socket=Socket, transport=Transport, %% the one previously received. goaway(State0=#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine, status=Status, streams=Streams0, stream_refs=Refs}, {goaway, LastStreamID, Reason, _}) -> - {Streams, RemovedRefs} = goaway_streams(State0, maps:to_list(Streams0), LastStreamID, - {goaway, Reason, 'The connection is going away.'}, [], []), + {Streams, RemovedRefs, HTTP2Machine1} = goaway_streams(State0, maps:to_list(Streams0), + LastStreamID, {goaway, Reason, 'The connection is going away.'}, [], [], HTTP2Machine), State = State0#http2_state{ + http2_machine=HTTP2Machine1, streams=maps:from_list(Streams), stream_refs=maps:without(RemovedRefs, Refs) }, @@ -926,15 +927,17 @@ goaway(State0=#http2_state{socket=Socket, transport=Transport, http2_machine=HTT {state, State} end. -%% Cancel server-initiated streams that are above LastStreamID. -goaway_streams(_, [], _, _, Acc, RefsAcc) -> - {Acc, RefsAcc}; -goaway_streams(State, [{StreamID, Stream=#stream{ref=StreamRef}}|Tail], LastStreamID, Reason, Acc, RefsAcc) +%% Cancel client-initiated streams that are above LastStreamID. +goaway_streams(_, [], _, _, Acc, RefsAcc, HTTP2Machine) -> + {Acc, RefsAcc, HTTP2Machine}; +goaway_streams(State, [{StreamID, Stream=#stream{ref=StreamRef}}|Tail], + LastStreamID, Reason, Acc, RefsAcc, HTTP2Machine) when StreamID > LastStreamID, (StreamID rem 2) =:= 1 -> close_stream(State, Stream, Reason), - goaway_streams(State, Tail, LastStreamID, Reason, Acc, [StreamRef|RefsAcc]); -goaway_streams(State, [StreamWithID|Tail], LastStreamID, Reason, Acc, RefsAcc) -> - goaway_streams(State, Tail, LastStreamID, Reason, [StreamWithID|Acc], RefsAcc). + {ok, HTTP2Machine1} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine), + goaway_streams(State, Tail, LastStreamID, Reason, Acc, [StreamRef|RefsAcc], HTTP2Machine1); +goaway_streams(State, [StreamWithID|Tail], LastStreamID, Reason, Acc, RefsAcc, HTTP2Machine) -> + goaway_streams(State, Tail, LastStreamID, Reason, [StreamWithID|Acc], RefsAcc, HTTP2Machine). %% We are already closing, do nothing. closing(_, #http2_state{status=closing}, _, EvHandlerState) -> diff --git a/test/shutdown_SUITE.erl b/test/shutdown_SUITE.erl index d28da687..fb4d3401 100644 --- a/test/shutdown_SUITE.erl +++ b/test/shutdown_SUITE.erl @@ -532,6 +532,74 @@ http2_server_goaway_many_streams(_) -> {response, fin, 200, _} = gun:await(ConnPid, StreamRef3), gun_is_down(ConnPid, ConnRef, normal). +goaway_window_update(_) -> + doc("HTTP/2: Confirm that gun does not crash when a GOAWAY frame " + "is followed by a connection-level WINDOW_UPDATE while " + "cow_http2_machine has buffered send data for a stream " + "removed by the GOAWAY handler."), + %% We use 'http' here because we need a custom handshake: with + %% initial_connection_window_size=65535 gun does not send the + %% connection-level WINDOW_UPDATE that http2_handshake/2 expects. + {ok, OriginPid, OriginPort} = init_origin(tcp, http, fun(Parent, ListenSocket, Socket, Transport) -> + %% Perform a custom HTTP/2 handshake. Advertise max_frame_size=32768 + %% so each 30720-byte POST body fits in a single DATA frame. + ok = Transport:send(Socket, cow_http2:settings(#{max_frame_size => 32768})), + {ok, <<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n">>} = Transport:recv(Socket, 24, 5000), + %% Receive the client SETTINGS. + {ok, <>} = Transport:recv(Socket, 3, 5000), + {ok, <<4:8, 0:40, _:Len/binary>>} = Transport:recv(Socket, 6 + Len, 5000), + ok = Transport:send(Socket, cow_http2:settings_ack()), + %% Receive the client SETTINGS ack. + {ok, <<0:24, 4:8, 1:8, 0:32>>} = Transport:recv(Socket, 9, 5000), + Parent ! {self(), origin_ready}, + %% Stream 1. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 5000), + {ok, _} = Transport:recv(Socket, HLen1, 5000), + %% Receive a DATA frame. + {ok, <>} = Transport:recv(Socket, 9, 5000), + {ok, _} = Transport:recv(Socket, DLen1, 5000), + %% Stream 3. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 5000), + {ok, _} = Transport:recv(Socket, HLen3, 5000), + %% Receive a DATA frame. + {ok, <>} = Transport:recv(Socket, 9, 5000), + {ok, _} = Transport:recv(Socket, DLen3, 5000), + %% Send 200 response for stream 1. + {HeadersBlock1, EncState0} = cow_hpack:encode([{<<":status">>, <<"200">>}]), + ok = Transport:send(Socket, cow_http2:headers(1, fin, HeadersBlock1)), + %% Send 200 response for stream 3. + {HeadersBlock3, _} = cow_hpack:encode([{<<":status">>, <<"200">>}], EncState0), + ok = Transport:send(Socket, cow_http2:headers(3, fin, HeadersBlock3)), + %% GOAWAY(3) + WINDOW_UPDATE(65535) in one write — the crash trigger. + ok = Transport:send(Socket, [ + cow_http2:goaway(3, no_error, <<>>), + cow_http2:window_update(65535) + ]), + gun_test:loop_origin(Parent, ListenSocket, Socket, Transport) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [http2], + retry => 0, + http2_opts => #{ + initial_connection_window_size => 65535, + initial_stream_window_size => 65535 + } + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + origin_ready = receive_from(OriginPid), + ConnRef = monitor(process, ConnPid), + Body = binary:copy(<<$x>>, 30720), + Headers = [{<<"content-type">>, <<"application/octet-stream">>}], + StreamRef1 = gun:post(ConnPid, "/req1", Headers, Body), + StreamRef2 = gun:post(ConnPid, "/req2", Headers, Body), + _StreamRef3 = gun:post(ConnPid, "/req3", Headers, Body), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), + gun_is_down(ConnPid, ConnRef, normal). + ws_gun_shutdown(Config) -> doc("Websocket: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1."),