From 70629673c534d907d99811d5357f74b82ae4a4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 3 Jun 2025 15:44:33 +0200 Subject: [PATCH] WIP close --- src/cowboy_http3.erl | 4 ++ src/cowboy_rest.erl | 1 + src/cowboy_webtransport.erl | 6 ++- test/draft_h3_webtransport_SUITE.erl | 62 ++++++++++++++++++++++++++++ test/handlers/wt_echo_h.erl | 4 +- 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/cowboy_http3.erl b/src/cowboy_http3.erl index 1a9f5256..f08bb15e 100644 --- a/src/cowboy_http3.erl +++ b/src/cowboy_http3.erl @@ -1047,6 +1047,10 @@ terminate_stream(State=#state{streams=Streams0, children=Children0}, Children = cowboy_children:shutdown(Children0, StreamID), stream_linger(State#state{streams=Streams, children=Children}, StreamID). +%% We must dereference the stream state when WebTransport is in use. +terminate_stream_handler(State, StreamID, Reason, + {cowboy_webtransport, #{stream_state := StreamState}}) -> + terminate_stream_handler(State, StreamID, Reason, StreamState); terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) -> try cowboy_stream:terminate(StreamID, Reason, StreamState) diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 1e4f4f72..9f30fcfe 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -1622,5 +1622,6 @@ error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, Class, erlang:raise(Class, Reason, Stacktrace). terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> + %% @todo I don't think the result is used anywhere? Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler), {ok, Req, Result}. diff --git a/src/cowboy_webtransport.erl b/src/cowboy_webtransport.erl index ef0fa837..437ab7d4 100644 --- a/src/cowboy_webtransport.erl +++ b/src/cowboy_webtransport.erl @@ -105,6 +105,7 @@ upgrade(Req=#{version := 'HTTP/3', pid := Pid, streamid := StreamID}, Env, Handl %% Use 501 Not Implemented to mirror the recommendation in %% by RFC9220 3 (WebSockets Upgrade over HTTP/3). false -> + %% @todo I don't think terminate will be called. {ok, cowboy_req:reply(501, Req), Env} end. @@ -212,7 +213,7 @@ commands([Command={close, _, _}|Tail], State, _, Acc) -> %% @todo set_options (to increase number of streams? data amounts? or a flow command?) %% @todo shutdown_reason if useful. -terminate(State, HandlerState, Reason) -> +terminate(State=#state{req=Req}, HandlerState, Reason) -> %cowboy_stream:terminate(StreamID, Reason, StreamState) %% @todo This terminate is at the connection level. % handler_terminate(State, HandlerState, Reason), @@ -223,7 +224,8 @@ terminate(State, HandlerState, Reason) -> % exit(normal). %handler_terminate(#state{handler=Handler, req=Req}, HandlerState, Reason) -> % cowboy_handler:terminate(Reason, Req, HandlerState, Handler). - ok. + %% @todo I think we must call terminate ourselves. + {ok, Req, Reason}. diff --git a/test/draft_h3_webtransport_SUITE.erl b/test/draft_h3_webtransport_SUITE.erl index ebe1d72a..4bbf2e37 100644 --- a/test/draft_h3_webtransport_SUITE.erl +++ b/test/draft_h3_webtransport_SUITE.erl @@ -246,6 +246,32 @@ datagrams(Config) -> %% An HTTP/3 GOAWAY frame is also a signal to applications to initiate shutdown for all WebTransport sessions. (4.6) +%% @todo Currently receipt of a GOAWAY frame immediately ends the connection. +%% We want to allow WT sessions to gracefully shut down before that. +%goaway_client(Config) -> +% doc("The HTTP/3 client can initiate the close of all WT sessions " +% "by sending a GOAWAY frame. (draft_webtrans_http3 4.6)"), +% %% Connect to the WebTransport server. +% #{ +% conn := Conn, +% connect_stream_ref := ConnectStreamRef, +% session_id := SessionID +% } = do_webtransport_connect(Config), +% %% Open a control stream and send a GOAWAY frame. +% {ok, ControlRef} = quicer:start_stream(Conn, +% #{open_flag => ?QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL}), +% {ok, SettingsBin, _HTTP3Machine0} = cow_http3_machine:init(client, #{}), +% {ok, _} = quicer:send(ControlRef, [ +% <<0>>, %% CONTROL stream. +% SettingsBin, +% <<7>>, %% GOAWAY frame. +% cow_http3:encode_int(1), +% cow_http3:encode_int(0) +% ]), +% %% Receive a datagram indicating processing by the WT handler. +% {datagram, SessionID, <<"TEST:close_initiated">>} = do_receive_datagram(Conn), +% ok. + drain_wt_session_client(Config) -> doc("The WT client can initiate the close of a single session. " "(draft_webtrans_http3 4.6)"), @@ -403,18 +429,54 @@ drain_wt_session_continue_server(Config) -> %% * a CLOSE_WEBTRANSPORT_SESSION capsule is either sent or received. %% (6) +%% @todo connect_stream_closed_cleanly_client +%% @todo connect_stream_closed_abruptly_client +%% @todo close_wt_session_client + +close_wt_session_server(Config) -> + doc("The WT server can close a single session. (draft_webtrans_http3 4.6)"), + %% Connect to the WebTransport server. + #{ + conn := Conn, + connect_stream_ref := ConnectStreamRef, + session_id := SessionID + } = do_webtransport_connect(Config), + %% Create a bidi stream, send a special instruction to make it initiate the close. + {ok, LocalStreamRef} = quicer:start_stream(Conn, #{}), + {ok, _} = quicer:send(LocalStreamRef, <<1:2, 16#41:14, 0:2, SessionID:6, "TEST:close">>), + %% Receive the CLOSE_WEBTRANSPORT_SESSION capsule on the CONNECT stream. + CloseWTSessionCapsule = cow_capsule:close_wt_session(0, <<>>), + {fin, CloseWTSessionCapsule} = do_receive_data(ConnectStreamRef), + ok. + %% Upon learning that the session has been terminated, the endpoint MUST reset the send side and abort reading on the receive side of all of the streams associated with the session (see Section 2.4 of [RFC9000]) using the WEBTRANSPORT_SESSION_GONE error code; it MUST NOT send any new datagrams or open any new streams. (6) +%% @todo wt_session_gone_client/server + %% To terminate a session with a detailed error message, an application MAY send an HTTP capsule [HTTP-DATAGRAM] of type CLOSE_WEBTRANSPORT_SESSION (0x2843). (6) +%% @todo close_wt_session_client/server %% Application Error Message: A UTF-8 encoded error message string provided by the application closing the session. The message takes up the remainder of the capsule, and its length MUST NOT exceed 1024 bytes. (6) +%% @todo close_wt_session_app_code_msg_client + +%% @todo +%close_wt_session_app_code_server(Config) -> +% error(todo). +%close_wt_session_app_code_msg_server(Config) -> +% error(todo). + %% An endpoint that sends a CLOSE_WEBTRANSPORT_SESSION capsule MUST immediately send a FIN. The endpoint MAY send a STOP_SENDING to indicate it is no longer reading from the CONNECT stream. The recipient MUST either close or reset the stream in response. (6) +%% @todo close_wt_session_server_fin +%% @todo The part about close/reset should be tested in close_wt_session_client. %% If any additional stream data is received on the CONNECT stream after receiving a CLOSE_WEBTRANSPORT_SESSION capsule, the stream MUST be reset with code H3_MESSAGE_ERROR. (6) +%% @todo close_wt_session_followed_by_data %% Cleanly terminating a CONNECT stream without a CLOSE_WEBTRANSPORT_SESSION capsule SHALL be semantically equivalent to terminating it with a CLOSE_WEBTRANSPORT_SESSION capsule that has an error code of 0 and an empty error string. (6) +%% @todo connect_stream_closed_app_code_0_empty_msg +%% @todo This one is about gracefully closing HTTP/3 connection with WT sessions. %% the endpoint SHOULD wait until all CONNECT streams have been closed by the peer before sending the CONNECTION_CLOSE (6) %% Helpers. diff --git a/test/handlers/wt_echo_h.erl b/test/handlers/wt_echo_h.erl index b65b91b9..e919e724 100644 --- a/test/handlers/wt_echo_h.erl +++ b/test/handlers/wt_echo_h.erl @@ -50,7 +50,9 @@ webtransport_handle(Event = {stream_data, _StreamID, _IsFin, <<"TEST:", Test/bit {[{open_stream, OpenStreamRef, bidi, <<>>}], Streams#{OpenStreamRef => bidi}}; <<"initiate_close">> -> - {[initiate_close], Streams} + {[initiate_close], Streams}; + <<"close">> -> + {[close], Streams} end; webtransport_handle(Event = {stream_data, StreamID, IsFin, Data}, Streams) -> ct:pal("WT handle ~p~n", [Event]),