mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add the idle_timeout HTTP/1.1 protocol option
This fixes the connection being dropped because of request_timeout despite there being some active streams.
This commit is contained in:
parent
73b4eb94ff
commit
95d2855f62
4 changed files with 57 additions and 35 deletions
|
@ -15,6 +15,7 @@ as a Ranch protocol.
|
||||||
----
|
----
|
||||||
opts() :: #{
|
opts() :: #{
|
||||||
env := cowboy_middleware:env(),
|
env := cowboy_middleware:env(),
|
||||||
|
idle_timeout := timeout(),
|
||||||
max_empty_lines := non_neg_integer(),
|
max_empty_lines := non_neg_integer(),
|
||||||
max_header_name_length := non_neg_integer(),
|
max_header_name_length := non_neg_integer(),
|
||||||
max_header_value_length := non_neg_integer(),
|
max_header_value_length := non_neg_integer(),
|
||||||
|
@ -43,6 +44,9 @@ The default value is given next to the option name:
|
||||||
env (#{})::
|
env (#{})::
|
||||||
Middleware environment.
|
Middleware environment.
|
||||||
|
|
||||||
|
idle_timeout (60000)::
|
||||||
|
Time in ms with no data received before Cowboy closes the connection.
|
||||||
|
|
||||||
max_empty_lines (5)::
|
max_empty_lines (5)::
|
||||||
Maximum number of empty lines before a request.
|
Maximum number of empty lines before a request.
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ init(Parent, Ref, Socket, Transport, Opts) ->
|
||||||
case Transport:peername(Socket) of
|
case Transport:peername(Socket) of
|
||||||
{ok, Peer} ->
|
{ok, Peer} ->
|
||||||
LastStreamID = maps:get(max_keepalive, Opts, 100),
|
LastStreamID = maps:get(max_keepalive, Opts, 100),
|
||||||
before_loop(set_request_timeout(#state{
|
before_loop(set_timeout(#state{
|
||||||
parent=Parent, ref=Ref, socket=Socket,
|
parent=Parent, ref=Ref, socket=Socket,
|
||||||
transport=Transport, opts=Opts,
|
transport=Transport, opts=Opts,
|
||||||
peer=Peer, last_streamid=LastStreamID}), <<>>);
|
peer=Peer, last_streamid=LastStreamID}), <<>>);
|
||||||
|
@ -150,12 +150,17 @@ before_loop(State=#state{socket=Socket, transport=Transport}, Buffer) ->
|
||||||
loop(State, Buffer).
|
loop(State, Buffer).
|
||||||
|
|
||||||
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
|
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
|
||||||
timer=TimerRef, children=Children}, Buffer) ->
|
timer=TimerRef, children=Children, streams=Streams}, Buffer) ->
|
||||||
{OK, Closed, Error} = Transport:messages(),
|
{OK, Closed, Error} = Transport:messages(),
|
||||||
receive
|
receive
|
||||||
%% Socket messages.
|
%% Socket messages.
|
||||||
{OK, Socket, Data} ->
|
{OK, Socket, Data} ->
|
||||||
parse(<< Buffer/binary, Data/binary >>, State);
|
%% Only reset the timeout if it is idle_timeout (active streams).
|
||||||
|
State1 = case Streams of
|
||||||
|
[] -> State;
|
||||||
|
_ -> set_timeout(State)
|
||||||
|
end,
|
||||||
|
parse(<< Buffer/binary, Data/binary >>, State1);
|
||||||
{Closed, Socket} ->
|
{Closed, Socket} ->
|
||||||
terminate(State, {socket_error, closed, 'The socket has been closed.'});
|
terminate(State, {socket_error, closed, 'The socket has been closed.'});
|
||||||
{Error, Socket, Reason} ->
|
{Error, Socket, Reason} ->
|
||||||
|
@ -200,13 +205,19 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
|
||||||
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
|
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
|
||||||
end.
|
end.
|
||||||
|
|
||||||
set_request_timeout(State0=#state{opts=Opts}) ->
|
%% We set request_timeout when there are no active streams,
|
||||||
State = cancel_request_timeout(State0),
|
%% and idle_timeout otherwise.
|
||||||
Timeout = maps:get(request_timeout, Opts, 5000),
|
set_timeout(State0=#state{opts=Opts, streams=Streams}) ->
|
||||||
TimerRef = erlang:start_timer(Timeout, self(), request_timeout),
|
State = cancel_timeout(State0),
|
||||||
|
{Name, Default} = case Streams of
|
||||||
|
[] -> {request_timeout, 5000};
|
||||||
|
_ -> {idle_timeout, 60000}
|
||||||
|
end,
|
||||||
|
Timeout = maps:get(Name, Opts, Default),
|
||||||
|
TimerRef = erlang:start_timer(Timeout, self(), Name),
|
||||||
State#state{timer=TimerRef}.
|
State#state{timer=TimerRef}.
|
||||||
|
|
||||||
cancel_request_timeout(State=#state{timer=TimerRef}) ->
|
cancel_timeout(State=#state{timer=TimerRef}) ->
|
||||||
ok = case TimerRef of
|
ok = case TimerRef of
|
||||||
undefined -> ok;
|
undefined -> ok;
|
||||||
_ -> erlang:cancel_timer(TimerRef, [{async, true}, {info, false}])
|
_ -> erlang:cancel_timer(TimerRef, [{async, true}, {info, false}])
|
||||||
|
@ -214,15 +225,15 @@ cancel_request_timeout(State=#state{timer=TimerRef}) ->
|
||||||
State#state{timer=undefined}.
|
State#state{timer=undefined}.
|
||||||
|
|
||||||
-spec timeout(_, _) -> no_return().
|
-spec timeout(_, _) -> no_return().
|
||||||
%% @todo Honestly it would be much better if we didn't enable pipelining yet.
|
|
||||||
timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) ->
|
timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) ->
|
||||||
%% @todo If other streams are running, just set the connection to be closed
|
terminate(State, {connection_error, timeout,
|
||||||
%% and stop trying to read from the socket?
|
'No request-line received before timeout.'});
|
||||||
terminate(State, {connection_error, timeout, 'No request-line received before timeout.'});
|
|
||||||
timeout(State=#state{in_state=#ps_header{}}, request_timeout) ->
|
timeout(State=#state{in_state=#ps_header{}}, request_timeout) ->
|
||||||
%% @todo If other streams are running, maybe wait for their reply before sending 408?
|
error_terminate(408, State, {connection_error, timeout,
|
||||||
%% -> Definitely. Either way, stop reading from the socket and make that stream the last.
|
'Request headers not received before timeout.'});
|
||||||
error_terminate(408, State, {connection_error, timeout, 'Request headers not received before timeout.'}).
|
timeout(State, idle_timeout) ->
|
||||||
|
terminate(State, {connection_error, timeout,
|
||||||
|
'Connection idle longer than configuration allows.'}).
|
||||||
|
|
||||||
%% Request-line.
|
%% Request-line.
|
||||||
parse(<<>>, State) ->
|
parse(<<>>, State) ->
|
||||||
|
@ -249,10 +260,11 @@ after_parse({request, Req=#{streamid := StreamID, headers := Headers, version :=
|
||||||
try cowboy_stream:init(StreamID, Req, Opts) of
|
try cowboy_stream:init(StreamID, Req, Opts) of
|
||||||
{Commands, StreamState} ->
|
{Commands, StreamState} ->
|
||||||
Streams = [#stream{id=StreamID, state=StreamState, version=Version}|Streams0],
|
Streams = [#stream{id=StreamID, state=StreamState, version=Version}|Streams0],
|
||||||
State = case maybe_req_close(State0, Headers, Version) of
|
State1 = case maybe_req_close(State0, Headers, Version) of
|
||||||
close -> State0#state{streams=Streams, last_streamid=StreamID};
|
close -> State0#state{streams=Streams, last_streamid=StreamID};
|
||||||
keepalive -> State0#state{streams=Streams}
|
keepalive -> State0#state{streams=Streams}
|
||||||
end,
|
end,
|
||||||
|
State = set_timeout(State1),
|
||||||
parse(Buffer, commands(State, StreamID, Commands))
|
parse(Buffer, commands(State, StreamID, Commands))
|
||||||
catch Class:Reason ->
|
catch Class:Reason ->
|
||||||
error_logger:error_msg("Exception occurred in "
|
error_logger:error_msg("Exception occurred in "
|
||||||
|
@ -617,13 +629,13 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream
|
||||||
false ->
|
false ->
|
||||||
State = case HasBody of
|
State = case HasBody of
|
||||||
true ->
|
true ->
|
||||||
cancel_request_timeout(State0#state{in_state=#ps_body{
|
State0#state{in_state=#ps_body{
|
||||||
%% @todo Don't need length anymore?
|
%% @todo Don't need length anymore?
|
||||||
transfer_decode_fun = TDecodeFun,
|
transfer_decode_fun = TDecodeFun,
|
||||||
transfer_decode_state = TDecodeState
|
transfer_decode_state = TDecodeState
|
||||||
}});
|
}};
|
||||||
false ->
|
false ->
|
||||||
set_request_timeout(State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}})
|
State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}
|
||||||
end,
|
end,
|
||||||
{request, Req, State, Buffer};
|
{request, Req, State, Buffer};
|
||||||
{true, HTTP2Settings} ->
|
{true, HTTP2Settings} ->
|
||||||
|
@ -661,7 +673,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
|
||||||
opts=Opts, peer=Peer}, Buffer) ->
|
opts=Opts, peer=Peer}, Buffer) ->
|
||||||
case Transport:secure() of
|
case Transport:secure() of
|
||||||
false ->
|
false ->
|
||||||
_ = cancel_request_timeout(State),
|
_ = cancel_timeout(State),
|
||||||
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer);
|
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer);
|
||||||
true ->
|
true ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
|
@ -676,7 +688,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
|
||||||
%% Always half-closed stream coming from this side.
|
%% Always half-closed stream coming from this side.
|
||||||
try cow_http_hd:parse_http2_settings(HTTP2Settings) of
|
try cow_http_hd:parse_http2_settings(HTTP2Settings) of
|
||||||
Settings ->
|
Settings ->
|
||||||
_ = cancel_request_timeout(State),
|
_ = cancel_timeout(State),
|
||||||
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
|
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
|
||||||
catch _:_ ->
|
catch _:_ ->
|
||||||
error_terminate(400, State, {connection_error, protocol_error,
|
error_terminate(400, State, {connection_error, protocol_error,
|
||||||
|
@ -705,10 +717,10 @@ parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=
|
||||||
{data, StreamID, nofin, Data, State#state{in_state=
|
{data, StreamID, nofin, Data, State#state{in_state=
|
||||||
PS#ps_body{transfer_decode_state=TState}}, Rest};
|
PS#ps_body{transfer_decode_state=TState}}, Rest};
|
||||||
{done, TotalLength, Rest} ->
|
{done, TotalLength, Rest} ->
|
||||||
{data, StreamID, {fin, TotalLength}, <<>>, set_request_timeout(
|
{data, StreamID, {fin, TotalLength}, <<>>, set_timeout(
|
||||||
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest};
|
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest};
|
||||||
{done, Data, TotalLength, Rest} ->
|
{done, Data, TotalLength, Rest} ->
|
||||||
{data, StreamID, {fin, TotalLength}, Data, set_request_timeout(
|
{data, StreamID, {fin, TotalLength}, Data, set_timeout(
|
||||||
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest}
|
State#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}}), Rest}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
@ -857,7 +869,7 @@ commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transpor
|
||||||
[{switch_protocol, Headers, Protocol, InitialState}|_Tail]) ->
|
[{switch_protocol, Headers, Protocol, InitialState}|_Tail]) ->
|
||||||
%% @todo This should be the last stream running otherwise we need to wait before switching.
|
%% @todo This should be the last stream running otherwise we need to wait before switching.
|
||||||
%% @todo If there's streams opened after this one, fail instead of 101.
|
%% @todo If there's streams opened after this one, fail instead of 101.
|
||||||
State = cancel_request_timeout(State0),
|
State = cancel_timeout(State0),
|
||||||
%% @todo When we actually do the upgrade, we only have the one stream left, plus
|
%% @todo When we actually do the upgrade, we only have the one stream left, plus
|
||||||
%% possibly some processes terminating. We need a smart strategy for handling the
|
%% possibly some processes terminating. We need a smart strategy for handling the
|
||||||
%% children shutdown. We can start with brutal_kill and discarding the EXIT messages
|
%% children shutdown. We can start with brutal_kill and discarding the EXIT messages
|
||||||
|
@ -918,7 +930,7 @@ stream_terminate(State0=#state{socket=Socket, transport=Transport,
|
||||||
streams=Streams0, children=Children0}, StreamID, Reason) ->
|
streams=Streams0, children=Children0}, StreamID, Reason) ->
|
||||||
{value, #stream{state=StreamState, version=Version}, Streams}
|
{value, #stream{state=StreamState, version=Version}, Streams}
|
||||||
= lists:keytake(StreamID, #stream.id, Streams0),
|
= lists:keytake(StreamID, #stream.id, Streams0),
|
||||||
State = case OutState of
|
State1 = case OutState of
|
||||||
wait ->
|
wait ->
|
||||||
info(State0, StreamID, {response, 204, #{}, <<>>});
|
info(State0, StreamID, {response, 204, #{}, <<>>});
|
||||||
chunked when Version =:= 'HTTP/1.1' ->
|
chunked when Version =:= 'HTTP/1.1' ->
|
||||||
|
@ -927,6 +939,11 @@ stream_terminate(State0=#state{socket=Socket, transport=Transport,
|
||||||
_ -> %% done or Version =:= 'HTTP/1.0'
|
_ -> %% done or Version =:= 'HTTP/1.0'
|
||||||
State0
|
State0
|
||||||
end,
|
end,
|
||||||
|
%% We reset the timeout if there are no active streams anymore.
|
||||||
|
State = case Streams of
|
||||||
|
[] -> set_timeout(State1);
|
||||||
|
_ -> State1
|
||||||
|
end,
|
||||||
|
|
||||||
stream_call_terminate(StreamID, Reason, StreamState),
|
stream_call_terminate(StreamID, Reason, StreamState),
|
||||||
%% @todo initiate children shutdown
|
%% @todo initiate children shutdown
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
%% This module implements a loop handler that sends
|
%% This module implements a loop handler that sends
|
||||||
%% itself a timeout that will intentionally arrive
|
%% itself a timeout that will intentionally arrive
|
||||||
%% too late, as it configures itself to only wait
|
%% after the HTTP/1.1 request_timeout. The protocol
|
||||||
%% 200ms before closing the connection in init/2.
|
%% is not supposed to close the connection when a
|
||||||
%% This results in a 204 reply being sent back by Cowboy.
|
%% request is ongoing, and therefore this handler
|
||||||
|
%% will eventually send a 200 reply.
|
||||||
|
|
||||||
-module(loop_handler_timeout_h).
|
-module(loop_handler_timeout_h).
|
||||||
|
|
||||||
|
@ -11,11 +12,11 @@
|
||||||
-export([terminate/3]).
|
-export([terminate/3]).
|
||||||
|
|
||||||
init(Req, _) ->
|
init(Req, _) ->
|
||||||
erlang:send_after(1000, self(), timeout),
|
erlang:send_after(6000, self(), timeout),
|
||||||
{cowboy_loop, Req, undefined, hibernate}.
|
{cowboy_loop, Req, #{hibernate => true}}.
|
||||||
|
|
||||||
info(timeout, Req, State) ->
|
info(timeout, Req, State) ->
|
||||||
{stop, cowboy_req:reply(500, Req), State}.
|
{stop, cowboy_req:reply(200, #{}, <<"Good!">>, Req), State}.
|
||||||
|
|
||||||
terminate(timeout, _, _) ->
|
terminate(stop, _, _) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
|
@ -83,9 +83,9 @@ loop_body(Config) ->
|
||||||
{response, fin, 200, _} = gun:await(ConnPid, Ref),
|
{response, fin, 200, _} = gun:await(ConnPid, Ref),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
loop_timeout(Config) ->
|
loop_request_timeout(Config) ->
|
||||||
doc("Ensure that the loop handler timeout results in a 204 response."),
|
doc("Ensure that the request_timeout isn't applied when a request is ongoing."),
|
||||||
ConnPid = gun_open(Config),
|
ConnPid = gun_open(Config),
|
||||||
Ref = gun:get(ConnPid, "/loop_timeout", [{<<"accept-encoding">>, <<"gzip">>}]),
|
Ref = gun:get(ConnPid, "/loop_timeout", [{<<"accept-encoding">>, <<"gzip">>}]),
|
||||||
{response, fin, 204, _} = gun:await(ConnPid, Ref),
|
{response, nofin, 200, _} = gun:await(ConnPid, Ref, 10000),
|
||||||
ok.
|
ok.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue