Browse Source

Handle expect: 100-continue request headers

The 100 continue response will only be sent if the client
has not sent the body yet (at all), if the connection is
HTTP/1.1 or above and if the user has not sent it yet.

The 100 continue response is sent when the user calls
read_body and it is cowboy_stream_h's responsibility
to send it. This means projects that don't use the
cowboy_stream_h stream handler will need to handle the
expect header themselves (but that's okay because they
might have different considerations than normal Cowboy).
Loïc Hoguin 7 years ago
parent
commit
217fac7f44
3 changed files with 77 additions and 7 deletions
  1. 46 7
      src/cowboy_stream_h.erl
  2. 3 0
      test/handlers/echo_h.erl
  3. 28 0
      test/req_SUITE.erl

+ 46 - 7
src/cowboy_stream_h.erl

@@ -30,6 +30,7 @@
 -record(state, {
 	ref = undefined :: ranch:ref(),
 	pid = undefined :: pid(),
+	expect = undefined :: undefined | continue,
 	read_body_ref = undefined :: reference() | undefined,
 	read_body_timer_ref = undefined :: reference() | undefined,
 	read_body_length = 0 :: non_neg_integer() | infinity,
@@ -49,18 +50,35 @@ init(_StreamID, Req=#{ref := Ref}, Opts) ->
 	Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]),
 	Shutdown = maps:get(shutdown_timeout, Opts, 5000),
 	Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env, Middlewares]),
-	{[{spawn, Pid, Shutdown}], #state{ref=Ref, pid=Pid}}.
+	Expect = expect(Req),
+	{[{spawn, Pid, Shutdown}], #state{ref=Ref, pid=Pid, expect=Expect}}.
+
+%% Ignore the expect header in HTTP/1.0.
+expect(#{version := 'HTTP/1.0'}) ->
+	undefined;
+expect(Req) ->
+	try cowboy_req:parse_header(<<"expect">>, Req) of
+		Expect ->
+			Expect
+	catch _:_ ->
+		undefined
+	end.
 
 %% If we receive data and stream is waiting for data:
 %%	If we accumulated enough data or IsFin=fin, send it.
 %%	If not, buffer it.
 %% If not, buffer it.
+%%
+%% We always reset the expect field when we receive data,
+%% since the client started sending the request body before
+%% we could send a 100 continue response.
 
 -spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
 	-> {cowboy_stream:commands(), State} when State::#state{}.
 data(_StreamID, IsFin, Data, State=#state{
 		read_body_ref=undefined, read_body_buffer=Buffer, body_length=BodyLen}) ->
 	{[], State#state{
+		expect=undefined,
 		read_body_is_fin=IsFin,
 		read_body_buffer= << Buffer/binary, Data/binary >>,
 		body_length=BodyLen + byte_size(Data)}};
@@ -68,6 +86,7 @@ data(_StreamID, nofin, Data, State=#state{
 		read_body_length=ReadLen, read_body_buffer=Buffer, body_length=BodyLen})
 		when byte_size(Data) + byte_size(Buffer) < ReadLen ->
 	{[], State#state{
+		expect=undefined,
 		read_body_buffer= << Buffer/binary, Data/binary >>,
 		body_length=BodyLen + byte_size(Data)}};
 data(_StreamID, IsFin, Data, State=#state{pid=Pid, read_body_ref=Ref,
@@ -76,6 +95,7 @@ data(_StreamID, IsFin, Data, State=#state{pid=Pid, read_body_ref=Ref,
 	ok = erlang:cancel_timer(TRef, [{async, true}, {info, false}]),
 	send_request_body(Pid, Ref, IsFin, BodyLen, <<Buffer/binary, Data/binary>>),
 	{[], State#state{
+		expect=undefined,
 		read_body_ref=undefined,
 		read_body_timer_ref=undefined,
 		read_body_buffer= <<>>,
@@ -102,15 +122,25 @@ info(StreamID, Exit = {'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref,
 		{internal_error, Exit, 'Stream process crashed.'}
 	], State};
 %% Request body, body buffered large enough or complete.
+%%
+%% We do not send a 100 continue response if the client
+%% already started sending the body.
 info(_StreamID, {read_body, Ref, Length, _}, State=#state{pid=Pid,
 		read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen})
 		when IsFin =:= fin; byte_size(Buffer) >= Length ->
 	send_request_body(Pid, Ref, IsFin, BodyLen, Buffer),
 	{[], State#state{read_body_buffer= <<>>}};
 %% Request body, not enough to send yet.
-info(StreamID, {read_body, Ref, Length, Period}, State) ->
+info(StreamID, {read_body, Ref, Length, Period}, State=#state{expect=Expect}) ->
+	Commands = case Expect of
+		continue -> [{inform, 100, #{}}, {flow, Length}];
+		undefined -> [{flow, Length}]
+	end,
 	TRef = erlang:send_after(Period, self(), {{self(), StreamID}, {read_body_timeout, Ref}}),
-	{[{flow, Length}], State#state{read_body_ref=Ref, read_body_timer_ref=TRef, read_body_length=Length}};
+	{Commands, State#state{
+		read_body_ref=Ref,
+		read_body_timer_ref=TRef,
+		read_body_length=Length}};
 %% Request body reading timeout; send what we got.
 info(_StreamID, {read_body_timeout, Ref}, State=#state{pid=Pid, read_body_ref=Ref,
 		read_body_is_fin=IsFin, read_body_buffer=Buffer, body_length=BodyLen}) ->
@@ -119,18 +149,27 @@ info(_StreamID, {read_body_timeout, Ref}, State=#state{pid=Pid, read_body_ref=Re
 info(_StreamID, {read_body_timeout, _}, State) ->
 	{[], State};
 %% Response.
-info(_StreamID, Inform = {inform, _, _}, State) ->
+%%
+%% We reset the expect field when a 100 continue response
+%% is sent or when any final response is sent.
+info(_StreamID, Inform = {inform, Status, _}, State0) ->
+	State = case Status of
+		100 -> State0#state{expect=undefined};
+		<<"100">> -> State0#state{expect=undefined};
+		<<"100 ", _/bits>> -> State0#state{expect=undefined};
+		_ -> State0
+	end,
 	{[Inform], State};
 info(_StreamID, Response = {response, _, _, _}, State) ->
-	{[Response], State};
+	{[Response], State#state{expect=undefined}};
 info(_StreamID, Headers = {headers, _, _}, State) ->
-	{[Headers], State};
+	{[Headers], State#state{expect=undefined}};
 info(_StreamID, Data = {data, _, _}, State) ->
 	{[Data], State};
 info(_StreamID, Push = {push, _, _, _, _, _, _, _}, State) ->
 	{[Push], State};
 info(_StreamID, SwitchProtocol = {switch_protocol, _, _, _}, State) ->
-	{[SwitchProtocol], State};
+	{[SwitchProtocol], State#state{expect=undefined}};
 %% Stray message.
 info(_StreamID, _Info, State) ->
 	{[], State}.

+ 3 - 0
test/handlers/echo_h.erl

@@ -18,6 +18,9 @@ echo(<<"read_body">>, Req0, Opts) ->
 		_ -> ok
 	end,
 	{_, Body, Req} = case cowboy_req:path(Req0) of
+		<<"/100-continue", _/bits>> ->
+			cowboy_req:inform(100, Req0),
+			cowboy_req:read_body(Req0);
 		<<"/full", _/bits>> -> read_body(Req0, <<>>);
 		<<"/opts", _/bits>> -> cowboy_req:read_body(Req0, Opts);
 		_ -> cowboy_req:read_body(Req0)

+ 28 - 0
test/req_SUITE.erl

@@ -54,6 +54,7 @@ init_dispatch(Config) ->
 		{"/opts/:key/length", echo_h, #{length => 1000}},
 		{"/opts/:key/period", echo_h, #{length => 999999999, period => 1000}},
 		{"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}},
+		{"/100-continue/:key", echo_h, []},
 		{"/full/:key", echo_h, []},
 		{"/no/:key", echo_h, []},
 		{"/direct/:key/[...]", echo_h, []},
@@ -456,6 +457,33 @@ do_read_body_timeout(Path, Body, Config) ->
 	{response, _, 500, _} = gun:await(ConnPid, Ref),
 	gun:close(ConnPid).
 
+read_body_expect_100_continue(Config) ->
+	doc("Request body with a 100-continue expect header."),
+	do_read_body_expect_100_continue("/read_body", Config).
+
+read_body_expect_100_continue_user_sent(Config) ->
+	doc("Request body with a 100-continue expect header, 100 response sent by handler."),
+	do_read_body_expect_100_continue("/100-continue/read_body", Config).
+
+do_read_body_expect_100_continue(Path, Config) ->
+	ConnPid = gun_open(Config),
+	Body = <<0:8000000>>,
+	Headers = [
+		{<<"accept-encoding">>, <<"gzip">>},
+		{<<"expect">>, <<"100-continue">>},
+		{<<"content-length">>, integer_to_binary(byte_size(Body))}
+	],
+	Ref = gun:post(ConnPid, Path, Headers),
+	{inform, 100, []} = gun:await(ConnPid, Ref),
+	gun:data(ConnPid, Ref, fin, Body),
+	{response, IsFin, 200, RespHeaders} = gun:await(ConnPid, Ref),
+	{ok, RespBody} = case IsFin of
+		nofin -> gun:await_body(ConnPid, Ref);
+		fin -> {ok, <<>>}
+	end,
+	gun:close(ConnPid),
+	do_decode(RespHeaders, RespBody).
+
 read_urlencoded_body(Config) ->
 	doc("application/x-www-form-urlencoded request body."),
 	<<"[]">> = do_body("POST", "/read_urlencoded_body", [], <<>>, Config),