From 217fac7f4414f5ff5eda85079a179e2462aba61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 30 Oct 2017 16:21:25 +0000 Subject: 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). --- src/cowboy_stream_h.erl | 53 +++++++++++++++++++++++++++++++++++++++++------- test/handlers/echo_h.erl | 3 +++ test/req_SUITE.erl | 28 +++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl index c631c87..8cd1457 100644 --- a/src/cowboy_stream_h.erl +++ b/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, <>), {[], 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}. diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl index b7a407b..18bdbe6 100644 --- a/test/handlers/echo_h.erl +++ b/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) diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 2d1fe38..107cdd8 100644 --- a/test/req_SUITE.erl +++ b/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), -- cgit v1.2.3