aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/cowboy_handler.erl77
-rw-r--r--src/cowboy_req.erl6
-rw-r--r--test/http_SUITE.erl16
-rw-r--r--test/http_handler_long_polling.erl2
4 files changed, 84 insertions, 17 deletions
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
index 7ed7db3..65e22b7 100644
--- a/src/cowboy_handler.erl
+++ b/src/cowboy_handler.erl
@@ -18,6 +18,16 @@
%% environment values. The result of this execution is added to the
%% environment under the <em>result</em> value.
%%
+%% When using loop handlers, we are receiving data from the socket because we
+%% want to know when the socket gets closed. This is generally not an issue
+%% because these kinds of requests are generally not pipelined, and don't have
+%% a body. If they do have a body, this body is often read in the
+%% <em>init/3</em> callback and this is no problem. Otherwise, this data
+%% accumulates in a buffer until we reach a certain threshold of 5000 bytes
+%% by default. This can be configured through the <em>loop_max_buffer</em>
+%% environment value. The request will be terminated with an
+%% <em>{error, overflow}</em> reason if this threshold is reached.
+%%
%% @see cowboy_http_handler
-module(cowboy_handler).
-behaviour(cowboy_middleware).
@@ -28,8 +38,10 @@
-record(state, {
env :: cowboy_middleware:env(),
hibernate = false :: boolean(),
+ loop_buffer_size = 0 :: non_neg_integer(),
+ loop_max_buffer = 5000 :: non_neg_integer() | infinity,
loop_timeout = infinity :: timeout(),
- loop_timeout_ref :: undefined | reference(),
+ loop_timeout_ref = undefined :: undefined | reference(),
resp_sent = false :: boolean()
}).
@@ -41,7 +53,12 @@
execute(Req, Env) ->
{_, Handler} = lists:keyfind(handler, 1, Env),
{_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env),
- handler_init(Req, #state{env=Env}, Handler, HandlerOpts).
+ case lists:keyfind(loop_max_buffer, 1, Env) of
+ false -> MaxBuffer = 5000, ok;
+ {_, MaxBuffer} -> ok
+ end,
+ handler_init(Req, #state{env=Env, loop_max_buffer=MaxBuffer},
+ Handler, HandlerOpts).
-spec handler_init(Req, #state{}, module(), any())
-> {ok, Req, cowboy_middleware:env()}
@@ -53,17 +70,17 @@ handler_init(Req, State, Handler, HandlerOpts) ->
{ok, Req2, HandlerState} ->
handler_handle(Req2, State, Handler, HandlerState);
{loop, Req2, HandlerState} ->
- handler_before_loop(Req2, State#state{hibernate=false},
- Handler, HandlerState);
+ handler_before_loop(Req2, State, Handler, HandlerState);
{loop, Req2, HandlerState, hibernate} ->
handler_before_loop(Req2, State#state{hibernate=true},
Handler, HandlerState);
{loop, Req2, HandlerState, Timeout} ->
- handler_before_loop(Req2, State#state{loop_timeout=Timeout},
- Handler, HandlerState);
+ State2 = handler_loop_timeout(State#state{loop_timeout=Timeout}),
+ handler_before_loop(Req2, State2, Handler, HandlerState);
{loop, Req2, HandlerState, Timeout, hibernate} ->
- handler_before_loop(Req2, State#state{
- hibernate=true, loop_timeout=Timeout}, Handler, HandlerState);
+ State2 = handler_loop_timeout(State#state{
+ hibernate=true, loop_timeout=Timeout}),
+ handler_before_loop(Req2, State2, Handler, HandlerState);
{shutdown, Req2, HandlerState} ->
terminate_request(Req2, State, Handler, HandlerState,
{normal, shutdown});
@@ -123,12 +140,14 @@ handler_handle(Req, State, Handler, HandlerState) ->
| {error, 500, Req} | {suspend, module(), function(), [any()]}
when Req::cowboy_req:req().
handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
+ [Socket, Transport] = cowboy_req:get([socket, transport], Req),
+ Transport:setopts(Socket, [{active, once}]),
{suspend, ?MODULE, handler_loop,
- [Req, State2#state{hibernate=false}, Handler, HandlerState]};
+ [Req, State#state{hibernate=false}, Handler, HandlerState]};
handler_before_loop(Req, State, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
- handler_loop(Req, State2, Handler, HandlerState).
+ [Socket, Transport] = cowboy_req:get([socket, transport], Req),
+ Transport:setopts(Socket, [{active, once}]),
+ handler_loop(Req, State, Handler, HandlerState).
%% Almost the same code can be found in cowboy_websocket.
-spec handler_loop_timeout(#state{}) -> #state{}.
@@ -136,8 +155,10 @@ handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
State#state{loop_timeout_ref=undefined};
handler_loop_timeout(State=#state{loop_timeout=Timeout,
loop_timeout_ref=PrevRef}) ->
- _ = case PrevRef of undefined -> ignore; PrevRef ->
- erlang:cancel_timer(PrevRef) end,
+ _ = case PrevRef of
+ undefined -> ignore;
+ PrevRef -> erlang:cancel_timer(PrevRef)
+ end,
TRef = erlang:start_timer(Timeout, self(), ?MODULE),
State#state{loop_timeout_ref=TRef}.
@@ -146,16 +167,38 @@ handler_loop_timeout(State=#state{loop_timeout=Timeout,
-> {ok, Req, cowboy_middleware:env()}
| {error, 500, Req} | {suspend, module(), function(), [any()]}
when Req::cowboy_req:req().
-handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) ->
+handler_loop(Req, State=#state{loop_buffer_size=NbBytes,
+ loop_max_buffer=Threshold, loop_timeout_ref=TRef},
+ Handler, HandlerState) ->
+ [Socket, Transport] = cowboy_req:get([socket, transport], Req),
+ {OK, Closed, Error} = Transport:messages(),
receive
+ {OK, Socket, Data} ->
+ NbBytes2 = NbBytes + byte_size(Data),
+ if NbBytes2 > Threshold ->
+ _ = handler_terminate(Req, Handler, HandlerState,
+ {error, overflow}),
+ error_terminate(Req, State);
+ true ->
+ Req2 = cowboy_req:append_buffer(Data, Req),
+ State2 = handler_loop_timeout(State#state{
+ loop_buffer_size=NbBytes2}),
+ handler_loop(Req2, State2, Handler, HandlerState)
+ end;
+ {Closed, Socket} ->
+ terminate_request(Req, State, Handler, HandlerState,
+ {error, closed});
+ {Error, Socket, Reason} ->
+ terminate_request(Req, State, Handler, HandlerState,
+ {error, Reason});
{cowboy_req, resp_sent} ->
- handler_loop(Req, State#state{resp_sent=true},
+ handler_before_loop(Req, State#state{resp_sent=true},
Handler, HandlerState);
{timeout, TRef, ?MODULE} ->
terminate_request(Req, State, Handler, HandlerState,
{normal, timeout});
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
- handler_loop(Req, State, Handler, HandlerState);
+ handler_before_loop(Req, State, Handler, HandlerState);
Message ->
handler_call(Req, State, Handler, HandlerState, Message)
end.
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index c807a75..d416916 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -104,6 +104,7 @@
-export([ensure_response/2]).
%% Private setter/getter API.
+-export([append_buffer/2]).
-export([get/2]).
-export([set/2]).
-export([set_bindings/4]).
@@ -1066,6 +1067,11 @@ ensure_response(#http_req{socket=Socket, transport=Transport,
%% Private setter/getter API.
%% @private
+-spec append_buffer(binary(), Req) -> Req when Req::req().
+append_buffer(Suffix, Req=#http_req{buffer=Buffer}) ->
+ Req#http_req{buffer= << Buffer/binary, Suffix/binary >>}.
+
+%% @private
-spec get(atom(), req()) -> any(); ([atom()], req()) -> any().
get(List, Req) when is_list(List) ->
[g(Atom, Req) || Atom <- List];
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index afe62c3..209be7e 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -49,6 +49,7 @@
-export([onresponse_crash/1]).
-export([onresponse_reply/1]).
-export([pipeline/1]).
+-export([pipeline_long_polling/1]).
-export([rest_bad_accept/1]).
-export([rest_created_path/1]).
-export([rest_expires/1]).
@@ -112,6 +113,7 @@ groups() ->
nc_rand,
nc_zero,
pipeline,
+ pipeline_long_polling,
rest_bad_accept,
rest_created_path,
rest_expires,
@@ -432,6 +434,8 @@ The document has moved
<A HREF=\"http://www.google.co.il/\">here</A>.
</BODY></HTML>",
Tests = [
+ {102, <<"GET /long_polling HTTP/1.1\r\nHost: localhost\r\n"
+ "Content-Length: 5000\r\n\r\n", 0:5000/unit:8 >>},
{200, ["GET / HTTP/1.0\r\nHost: localhost\r\n"
"Set-Cookie: ", HugeCookie, "\r\n\r\n"]},
{200, "\r\n\r\n\r\n\r\n\r\nGET / HTTP/1.1\r\nHost: localhost\r\n\r\n"},
@@ -449,6 +453,8 @@ The document has moved
{408, "GET / HTTP/1.1\r\nHost: localhost\r\n\r"},
{414, Huge},
{400, "GET / HTTP/1.1\r\n" ++ Huge},
+ {500, <<"GET /long_polling HTTP/1.1\r\nHost: localhost\r\n"
+ "Content-Length: 100000\r\n\r\n", 0:100000/unit:8 >>},
{505, "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"},
{closed, ""},
{closed, "\r\n"},
@@ -758,6 +764,16 @@ pipeline(Config) ->
{ok, 200, _, Client11} = cowboy_client:response(Client10),
{error, closed} = cowboy_client:response(Client11).
+pipeline_long_polling(Config) ->
+ Client = ?config(client, Config),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/long_polling", Config), Client),
+ {ok, Client3} = cowboy_client:request(<<"GET">>,
+ build_url("/long_polling", Config), Client2),
+ {ok, 102, _, Client4} = cowboy_client:response(Client3),
+ {ok, 102, _, Client5} = cowboy_client:response(Client4),
+ {error, closed} = cowboy_client:response(Client5).
+
rest_bad_accept(Config) ->
Client = ?config(client, Config),
{ok, Client2} = cowboy_client:request(<<"GET">>,
diff --git a/test/http_handler_long_polling.erl b/test/http_handler_long_polling.erl
index 763e1fe..e8cf610 100644
--- a/test/http_handler_long_polling.erl
+++ b/test/http_handler_long_polling.erl
@@ -19,4 +19,6 @@ info(timeout, Req, State) ->
{loop, Req, State - 1, hibernate}.
terminate({normal, shutdown}, _, _) ->
+ ok;
+terminate({error, overflow}, _, _) ->
ok.