%% Copyright (c) 2019-2023, Loïc Hoguin %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -module(shutdown_SUITE). -compile(export_all). -compile(nowarn_export_all). -import(ct_helper, [doc/1]). -import(ct_helper, [config/2]). -import(gun_test, [init_origin/3]). -import(gun_test, [receive_from/1]). suite() -> [{timetrap, 30000}]. all() -> [{group, shutdown}]. groups() -> [{shutdown, [parallel], ct_helper:all(?MODULE)}]. init_per_suite(Config) -> ProtoOpts = #{env => #{ dispatch => cowboy_router:compile([{'_', [ {"/", hello_h, []}, {"/delayed", delayed_hello_h, 500}, {"/delayed_push", delayed_push_h, 500}, {"/empty", empty_h, []}, {"/ws", ws_echo_h, []}, {"/ws_frozen", ws_frozen_h, 500}, %% This timeout determines how long the test suite will run. {"/ws_frozen_long", ws_frozen_h, 1500}, {"/ws_timeout_close", ws_timeout_close_h, 500} ]}]) }}, {ok, _} = cowboy:start_clear(?MODULE, [], ProtoOpts), OriginPort = ranch:get_port(?MODULE), [{origin_port, OriginPort}|Config]. end_per_suite(_) -> ok = cowboy:stop_listener(?MODULE). %% Tests. %% %% This test suite checks that the various ways to shut down %% the connection are all working as expected for the different %% protocols and scenarios. not_connected_gun_shutdown(_) -> doc("Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 while it isn't connected."), {ok, ConnPid} = gun:open("localhost", 12345), ConnRef = monitor(process, ConnPid), gun:shutdown(ConnPid), gun_is_down(ConnPid, ConnRef, shutdown). not_connected_owner_down(_) -> doc("Confirm that the Gun process shuts down when the owner exits normally " "while it isn't connected."), do_not_connected_owner_down(normal, normal). not_connected_owner_down_error(_) -> doc("Confirm that the Gun process shuts down when the owner exits with an error " "while it isn't connected."), do_not_connected_owner_down(unexpected, {shutdown, {owner_down, unexpected}}). do_not_connected_owner_down(ExitReason, DownReason) -> Self = self(), spawn(fun() -> {ok, ConnPid} = gun:open("localhost", 12345), Self ! {conn, ConnPid}, timer:sleep(500), exit(ExitReason) end), ConnPid = receive {conn, C} -> C end, ConnRef = monitor(process, ConnPid), gun_is_down(ConnPid, ConnRef, DownReason). http1_gun_shutdown_no_streams(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with no active streams."), do_http_gun_shutdown_no_streams(Config, http). do_http_gun_shutdown_no_streams(Config, Protocol) -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), gun:shutdown(ConnPid), gun_is_down(ConnPid, ConnRef, shutdown). http1_gun_shutdown_one_stream(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with one active stream."), do_http_gun_shutdown_one_stream(Config, http). do_http_gun_shutdown_one_stream(Config, Protocol) -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/delayed"), gun:shutdown(ConnPid), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, _} = gun:await_body(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, shutdown). http1_gun_shutdown_pipelined_streams(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with one active stream and additional pipelined streams."), Protocol = http, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef1 = gun:get(ConnPid, "/delayed"), StreamRef2 = gun:get(ConnPid, "/delayed"), StreamRef3 = gun:get(ConnPid, "/delayed"), gun:shutdown(ConnPid), %% Pipelined streams are canceled immediately. {error, {stream_error, {closing, shutdown}}} = gun:await(ConnPid, StreamRef2), {error, {stream_error, {closing, shutdown}}} = gun:await(ConnPid, StreamRef3), %% The active stream is still processed. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {ok, _} = gun:await_body(ConnPid, StreamRef1), gun_is_down(ConnPid, ConnRef, shutdown). http1_gun_shutdown_timeout(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down when the closing_timeout " "triggers after calling gun:shutdown/1 with one active stream."), do_http_gun_shutdown_timeout(Config, http, http_opts). do_http_gun_shutdown_timeout(Config, Protocol, ProtoOpts) -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ ProtoOpts => #{closing_timeout => 100}, protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/delayed"), gun:shutdown(ConnPid), %% The closing timeout occurs before the server gets to send the response. %% We get a 'closed' error instead of 'closing' as a result. {error, {stream_error, {closed, shutdown}}} = gun:await(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, shutdown). http1_owner_down(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down when the owner exits normally."), do_http_owner_down(Config, http, normal, normal). http1_owner_down_error(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down when the owner exits with an error."), do_http_owner_down(Config, http, unexpected, {shutdown, {owner_down, unexpected}}). do_http_owner_down(Config, Protocol, ExitReason, DownReason) -> Self = self(), spawn(fun() -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), Self ! {conn, ConnPid}, {ok, Protocol} = gun:await_up(ConnPid), timer:sleep(500), exit(ExitReason) end), ConnPid = receive {conn, C} -> C end, ConnRef = monitor(process, ConnPid), gun_is_down(ConnPid, ConnRef, DownReason). http1_request_connection_close(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when sending a request with the connection: close header and " "retry is disabled."), Protocol = http, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/", #{ <<"connection">> => <<"close">> }), %% We get the response followed by Gun shutting down. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, _} = gun:await_body(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, normal). http1_request_connection_close_pipeline(Config) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when sending a request with the connection: close header and " "retry is disabled. Pipelined requests get canceled."), Protocol = http, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef1 = gun:get(ConnPid, "/", #{ <<"connection">> => <<"close">> }), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), %% We get the response, pipelined streams get canceled, followed by Gun shutting down. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef2), {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef3), {ok, _} = gun:await_body(ConnPid, StreamRef1), gun_is_down(ConnPid, ConnRef, normal). http1_response_connection_close(_) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when receiving a response with the connection: close header and " "retry is disabled."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{ env => #{dispatch => cowboy_router:compile([{'_', [{"/", hello_h, []}]}])}, max_keepalive => 1 }), OriginPort = ranch:get_port(?FUNCTION_NAME), try Protocol = http, {ok, ConnPid} = gun:open("localhost", OriginPort, #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/"), %% We get the response followed by Gun shutting down. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, _} = gun:await_body(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, normal) after cowboy:stop_listener(?FUNCTION_NAME) end. http1_response_connection_close_pipeline(_) -> doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " "when receiving a response with the connection: close header and " "retry is disabled. Pipelined requests get canceled."), {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{ env => #{dispatch => cowboy_router:compile([{'_', [{"/", hello_h, []}]}])}, max_keepalive => 1 }), OriginPort = ranch:get_port(?FUNCTION_NAME), try Protocol = http, {ok, ConnPid} = gun:open("localhost", OriginPort, #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), %% We get the response, pipelined streams get canceled, followed by Gun shutting down. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {ok, _} = gun:await_body(ConnPid, StreamRef1), {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef2), {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef3), gun_is_down(ConnPid, ConnRef, normal) after cowboy:stop_listener(?FUNCTION_NAME) end. http10_connection_close(Config) -> doc("HTTP/1.0: Confirm that the Gun process shuts down gracefully " "when sending a request without a connection header and " "retry is disabled."), Protocol = http, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ http_opts => #{version => 'HTTP/1.0'}, protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/"), %% We get the response followed by Gun shutting down. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, _} = gun:await_body(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, normal). http1_response_connection_close_delayed_body(_) -> doc("HTTP/1.1: Confirm that requests initiated when Gun has received a " "connection: close response header fail immediately if retry " "is disabled, without waiting for the response body."), ServerFun = fun(_, _, ClientSocket, gen_tcp) -> try {ok, Req} = gen_tcp:recv(ClientSocket, 0, 5000), <<"GET / HTTP/1.1\r\n", _/binary>> = Req, ok = gen_tcp:send(ClientSocket, <<"HTTP/1.1 200 OK\r\n" "Connection: close\r\n" "Content-Length: 12\r\n\r\nHello">>), timer:sleep(500), ok = gen_tcp:send(ClientSocket, " world!") after timer:sleep(1000), gen_tcp:close(ClientSocket) end end, {ok, ServerPid, OriginPort} = gun_test:init_origin(tcp, http, ServerFun), %% Client connects. {ok, ConnPid} = gun:open("localhost", OriginPort, #{ protocols => [http], retry => 0 }), {ok, _Protocol} = gun:await_up(ConnPid), receive {ServerPid, handshake_completed} -> ok end, ConnRef = monitor(process, ConnPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), %% We get the response headers with connection: close. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), %% Pipelined request fails immediately. {gun_error, ConnPid, StreamRef2, closing} = receive E2 -> E2 end, {gun_data, ConnPid, StreamRef1, nofin, <<"Hello">>} = receive PartialBody -> PartialBody end, %% Request initiated when Gun is in closing state fails immediately. StreamRef3 = gun:get(ConnPid, "/"), {gun_error, ConnPid, StreamRef3, closing} = receive E3 -> E3 end, {gun_data, ConnPid, StreamRef1, fin, <<" world!">>} = receive RestBody -> RestBody end, gun_is_down(ConnPid, ConnRef, normal). http2_gun_shutdown_no_streams(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with no active streams."), do_http_gun_shutdown_no_streams(Config, http2). http2_gun_shutdown_one_stream(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with one active stream."), do_http_gun_shutdown_one_stream(Config, http2). http2_gun_shutdown_many_streams(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with many active streams."), Protocol = http2, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef1 = gun:get(ConnPid, "/delayed"), StreamRef2 = gun:get(ConnPid, "/delayed"), StreamRef3 = gun:get(ConnPid, "/delayed"), gun:shutdown(ConnPid), %% All streams are processed. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), {ok, _} = gun:await_body(ConnPid, StreamRef1), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {ok, _} = gun:await_body(ConnPid, StreamRef2), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), {ok, _} = gun:await_body(ConnPid, StreamRef3), gun_is_down(ConnPid, ConnRef, shutdown). http2_gun_shutdown_timeout(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down when the closing_timeout " "triggers after calling gun:shutdown/1 with one active stream."), do_http_gun_shutdown_timeout(Config, http2, http2_opts). http2_gun_shutdown_ignore_push_promise(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 with one active stream. The " "resource pushed by the server after we sent the GOAWAY frame " "must be ignored."), Protocol = http2, {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ protocols => [Protocol] }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:get(ConnPid, "/delayed_push"), gun:shutdown(ConnPid), %% We do not receive the push streams. Only the response. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), {ok, _} = gun:await_body(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, shutdown). http2_owner_down(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down when the owner exits normally."), do_http_owner_down(Config, http2, normal, normal). http2_owner_down_error(Config) -> doc("HTTP/2: Confirm that the Gun process shuts down when the owner exits with an error."), do_http_owner_down(Config, http2, unexpected, {shutdown, {owner_down, unexpected}}). http2_server_goaway_no_streams(_) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when receiving a GOAWAY frame with no active streams and " "retry is disabled."), {ok, OriginPid, Port} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> receive go_away -> ok end, Transport:send(Socket, cow_http2:goaway(0, no_error, <<>>)), timer:sleep(500) end), Protocol = http2, {ok, ConnPid} = gun:open("localhost", Port, #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), OriginPid ! go_away, gun_is_down(ConnPid, ConnRef, normal). http2_server_goaway_one_stream(_) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when receiving a GOAWAY frame with one active stream and " "retry is disabled."), {ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> %% Receive a HEADERS frame. {ok, <>} = Transport:recv(Socket, 9, 1000), %% Skip the header. {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), %% Send a GOAWAY frame. Transport:send(Socket, cow_http2:goaway(1, no_error, <<>>)), %% Wait before sending the response back and closing the connection. timer:sleep(500), %% Send a HEADERS frame. {HeadersBlock, _} = cow_hpack:encode([ {<<":status">>, <<"200">>} ]), ok = Transport:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock) ]), timer:sleep(500) end), Protocol = http2, {ok, ConnPid} = gun:open("localhost", OriginPort, #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), handshake_completed = receive_from(OriginPid), StreamRef = gun:get(ConnPid, "/"), ConnRef = monitor(process, ConnPid), {response, fin, 200, _} = gun:await(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, normal). http2_server_goaway_many_streams(_) -> doc("HTTP/2: Confirm that the Gun process shuts down gracefully " "when receiving a GOAWAY frame with many active streams and " "retry is disabled."), {ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> %% Stream 1. %% Receive a HEADERS frame. {ok, <>} = Transport:recv(Socket, 9, 1000), %% Skip the header. {ok, _} = gen_tcp:recv(Socket, SkipLen1, 1000), %% Stream 2. %% Receive a HEADERS frame. {ok, <>} = Transport:recv(Socket, 9, 1000), %% Skip the header. {ok, _} = gen_tcp:recv(Socket, SkipLen2, 1000), %% Stream 3. %% Receive a HEADERS frame. {ok, <>} = Transport:recv(Socket, 9, 1000), %% Skip the header. {ok, _} = gen_tcp:recv(Socket, SkipLen3, 1000), %% Stream 4. %% Receive a HEADERS frame, but simulate that it is still %% in-flight when the GOAWAY frame is sent. {ok, <>} = Transport:recv(Socket, 9, 1000), %% Skip the header. {ok, _} = gen_tcp:recv(Socket, SkipLen4, 1000), %% Send a GOAWAY frame. Simulate that GOAWAY was sent before %% receiving stream 4 by including last stream ID of stream 3. Transport:send(Socket, cow_http2:goaway(5, no_error, <<>>)), %% Gun replies with GOAWAY. {ok, <>} = Transport:recv(Socket, 9, 1000), {ok, _SkippedPayload} = gen_tcp:recv(Socket, SkipLen5, 1000), timer:sleep(500), %% Send replies for streams 1-3. {HeadersBlock1, State0} = cow_hpack:encode([ {<<":status">>, <<"200">>} ]), ok = Transport:send(Socket, [ cow_http2:headers(1, fin, HeadersBlock1) ]), {HeadersBlock2, State} = cow_hpack:encode([ {<<":status">>, <<"200">>} ], State0), ok = Transport:send(Socket, [ cow_http2:headers(3, fin, HeadersBlock2) ]), {HeadersBlock3, _} = cow_hpack:encode([ {<<":status">>, <<"200">>} ], State), ok = Transport:send(Socket, [ cow_http2:headers(5, fin, HeadersBlock3) ]), %% Gun closes the connection. {error, closed} = gen_tcp:recv(Socket, 9) end), Protocol = http2, {ok, ConnPid} = gun:open("localhost", OriginPort, #{ protocols => [Protocol], retry => 0 }), {ok, Protocol} = gun:await_up(ConnPid), handshake_completed = receive_from(OriginPid), StreamRef1 = gun:get(ConnPid, "/"), StreamRef2 = gun:get(ConnPid, "/"), StreamRef3 = gun:get(ConnPid, "/"), StreamRef4 = gun:get(ConnPid, "/"), ConnRef = monitor(process, ConnPid), %% GOAWAY received. Stream 4 is cancelled. {gun_error, ConnPid, StreamRef4, Reason4} = receive E4 -> E4 end, {goaway, no_error, _} = Reason4, StreamRef5 = gun:get(ConnPid, "/"), {gun_error, ConnPid, StreamRef5, closing} = receive E5 -> E5 end, {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), {response, fin, 200, _} = gun:await(ConnPid, StreamRef3), 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."), {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), {ok, http} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), gun:shutdown(ConnPid), gun_is_down(ConnPid, ConnRef, shutdown). ws_gun_shutdown_timeout(Config) -> doc("Websocket: Confirm that the Gun process shuts down when " "the closing_timeout triggers after calling gun:shutdown/1."), {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ ws_opts => #{closing_timeout => 100} }), {ok, http} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen_long", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), gun:shutdown(ConnPid), gun_is_down(ConnPid, ConnRef, shutdown). ws_owner_down(Config) -> doc("Websocket: Confirm that the Gun process shuts down when the owner exits normally."), do_ws_owner_down(Config, normal, normal). ws_owner_down_error(Config) -> doc("Websocket: Confirm that the Gun process shuts down when the owner exits with an error."), do_ws_owner_down(Config, unexpected, {shutdown, {owner_down, unexpected}}). do_ws_owner_down(Config, ExitReason, DownReason) -> Self = self(), spawn(fun() -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), Self ! {conn, ConnPid}, {ok, http} = gun:await_up(ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), timer:sleep(500), exit(ExitReason) end), ConnPid = receive {conn, C} -> C end, ConnRef = monitor(process, ConnPid), gun_is_down(ConnPid, ConnRef, DownReason). ws_gun_send_close_frame(Config) -> doc("Websocket: Confirm that the Gun process shuts down gracefully " "when sending a close frame, with retry disabled."), {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ retry => 0 }), {ok, http} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), %% We send a close frame. We expect the same frame back %% before the connection is closed. Frame = {close, 3333, <<>>}, gun:ws_send(ConnPid, StreamRef, Frame), {ws, Frame} = gun:await(ConnPid, StreamRef), ws_is_down(ConnPid, StreamRef, normal), gun_is_down(ConnPid, ConnRef, normal). ws_gun_receive_close_frame(Config) -> doc("Websocket: Confirm that the Gun process shuts down gracefully " "when receiving a close frame, with retry disabled."), {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ retry => 0 }), {ok, http} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws_timeout_close", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), %% We expect a close frame before the connection is closed. {ws, {close, 3333, <<>>}} = gun:await(ConnPid, StreamRef), ws_is_down(ConnPid, StreamRef, normal), gun_is_down(ConnPid, ConnRef, normal). closing_gun_shutdown(Config) -> doc("Confirm that the Gun process shuts down gracefully " "when calling gun:shutdown/1 while Gun is closing a connection."), {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), {ok, http} = gun:await_up(ConnPid), ConnRef = monitor(process, ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), %% We send a close frame then immediately call gun:shutdown/1. %% We expect Gun to go down without retrying to reconnect. Frame = {close, 3333, <<>>}, gun:ws_send(ConnPid, StreamRef, Frame), gun:shutdown(ConnPid), {ws, Frame} = gun:await(ConnPid, StreamRef), gun_is_down(ConnPid, ConnRef, shutdown). closing_owner_down(Config) -> doc("Confirm that the Gun process shuts down gracefully " "when the owner exits normally while Gun is closing a connection."), do_closing_owner_down(Config, normal, normal). closing_owner_down_error(Config) -> doc("Confirm that the Gun process shuts down gracefully " "when the owner exits with an error while Gun is closing a connection."), do_closing_owner_down(Config, unexpected, {shutdown, {owner_down, unexpected}}). do_closing_owner_down(Config, ExitReason, DownReason) -> Self = self(), spawn(fun() -> {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), Self ! {conn, ConnPid}, {ok, http} = gun:await_up(ConnPid), StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen", []), {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), gun:ws_send(ConnPid, StreamRef, {close, 3333, <<>>}), timer:sleep(100), exit(ExitReason) end), ConnPid = receive {conn, C} -> C end, ConnRef = monitor(process, ConnPid), gun_is_down(ConnPid, ConnRef, DownReason). %% Internal. gun_is_down(ConnPid, ConnRef, Expected) -> receive {'DOWN', ConnRef, process, ConnPid, Reason} -> Expected = Reason, ok end. ws_is_down(ConnPid, StreamRef, Expected) -> receive {gun_down, ConnPid, ws, Reason, StreamsDown} -> Expected = Reason, [StreamRef] = StreamsDown, ok end.