diff --git a/test/handlers/loop_handler_endless_h.erl b/test/handlers/loop_handler_endless_h.erl new file mode 100644 index 00000000..3b11ba4f --- /dev/null +++ b/test/handlers/loop_handler_endless_h.erl @@ -0,0 +1,24 @@ +%% This module implements a loop handler that streams endless data + +-module(loop_handler_endless_h). + +-export([init/2]). +-export([info/3]). + +init(Req0, #{delay := Delay} = Opts) -> + case cowboy_req:header(<<"x-test-pid">>, Req0) of + BinPid when is_binary(BinPid) -> + Pid = list_to_pid(binary_to_list(BinPid)), + Pid ! {stream, self()}, + ok; + _ -> + ok + end, + erlang:send_after(Delay, self(), timeout), + Req = cowboy_req:stream_reply(200, Req0), + {cowboy_loop, Req, Opts}. + +info(timeout, Req, State) -> + cowboy_req:stream_body(<<0:1000/unit:8>>, nofin, Req), + erlang:send_after(10, self(), timeout), + {ok, Req, State}. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index d0c92e45..4ccc8426 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -514,3 +514,70 @@ graceful_shutdown_listener(Config) -> %% Check that the 2nd (very slow) request is not handled. {error, {stream_error, closed}} = gun:await(ConnPid2, Ref2), gun:close(ConnPid2). + +send_timeout_close(_Config) -> + doc("Check that connections are closed on send timeout."), + Path = "/endless_stream", + Dispatch = cowboy_router:compile([{"localhost", [ + {Path, loop_handler_endless_h, #{delay => 100}} + ]}]), + ProtoOpts = #{ + env => #{dispatch => Dispatch}, + idle_timeout => infinity + }, + TransOpts = #{ + port => 0, + socket_opts => [ + {send_timeout, 100}, + {send_timeout_close, true}, + {sndbuf, 10} + ] + }, + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, TransOpts, ProtoOpts), + Port = ranch:get_port(?FUNCTION_NAME), + ClientPid = do_hang_get(Port, Path), + StreamPid = receive {stream, Pid} -> Pid end, + {links, [HttpPid]} = erlang:process_info(StreamPid, links), + [Socket] = [ + PidOrPort + || {links, Links} <- [erlang:process_info(HttpPid, links)], + PidOrPort <- Links, + is_port(PidOrPort) + ], + WaitClosed = + fun F(T, Status) when T =< 0 -> + erlang:error({status, Status}); + F(T, _) -> + case prim_inet:getstatus(Socket) of + {error, _} -> + ok; + {ok, Status} -> + Snooze = 100, + ct:sleep(Snooze), + F(T - Snooze, Status) + end + end, + ok = WaitClosed(2000, undefined), + false = erlang:is_process_alive(StreamPid), + false = erlang:is_process_alive(HttpPid), + ClientPid ! done. + +%% do_hang_get/2 issues a GET request but doesn't read response +do_hang_get(Port, Path) -> + Parent = self(), + erlang:spawn_link( + fun() -> + Opts = [{recbuf, 10}, {buffer, 10}, {active, false}, {packet, 0}], + {ok, S} = gen_tcp:connect("localhost", Port, Opts), + Req = [ + "GET ", Path, " HTTP/1.1\r\n", + "Host: localhost:", erlang:integer_to_list(Port), "\r\n", + "x-test-pid: ", erlang:pid_to_list(Parent), "\r\n\r\n" + ], + ok = gen_tcp:send(S, Req), + receive + done -> ok + end, + gen_tcp:close(S) + end + ).