aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/cowboy_compress_h.erl7
-rw-r--r--src/cowboy_http.erl90
-rw-r--r--src/cowboy_stream.erl24
-rw-r--r--src/cowboy_stream_h.erl7
4 files changed, 100 insertions, 28 deletions
diff --git a/src/cowboy_compress_h.erl b/src/cowboy_compress_h.erl
index 229e75f..9c2ae34 100644
--- a/src/cowboy_compress_h.erl
+++ b/src/cowboy_compress_h.erl
@@ -19,6 +19,7 @@
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
+-export([early_error/5]).
-record(state, {
next :: any(),
@@ -55,6 +56,12 @@ terminate(StreamID, Reason, #state{next=Next, deflate=Z}) ->
end,
cowboy_stream:terminate(StreamID, Reason, Next).
+-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
+ cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
+ when Resp::cowboy_stream:resp_command().
+early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
+ cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
+
%% Internal.
%% Check if the client supports decoding of gzip responses.
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index ac0d915..e6cceae 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -219,11 +219,10 @@ timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) ->
%% @todo If other streams are running, just set the connection to be closed
%% and stop trying to read from the socket?
terminate(State, {connection_error, timeout, 'No request-line received before timeout.'});
-timeout(State=#state{socket=Socket, transport=Transport, 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?
%% -> Definitely. Either way, stop reading from the socket and make that stream the last.
- Transport:send(Socket, cow_http:response(408, 'HTTP/1.1', [])),
- terminate(State, {connection_error, timeout, 'Request headers not received before timeout.'}).
+ error_terminate(408, State, {connection_error, timeout, 'Request headers not received before timeout.'}).
%% Request-line.
parse(<<>>, State) ->
@@ -440,11 +439,13 @@ parse_header(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) ->
NumHeaders = maps:size(Headers),
case match_colon(Buffer, 0) of
nomatch when byte_size(Buffer) > MaxLength ->
- error_terminate(431, State, {connection_error, limit_reached,
- 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
+ error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}},
+ {connection_error, limit_reached,
+ 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
nomatch when NumHeaders >= MaxHeaders ->
- error_terminate(431, State, {connection_error, limit_reached,
- 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
+ error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}},
+ {connection_error, limit_reached,
+ 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
nomatch ->
{more, State#state{in_state=PS#ps_header{headers=Headers}}, Buffer};
_ ->
@@ -460,9 +461,10 @@ match_colon(_, _) ->
parse_hd_name(<< $:, Rest/bits >>, State, H, SoFar) ->
parse_hd_before_value(Rest, State, H, SoFar);
-parse_hd_name(<< C, _/bits >>, State, _, <<>>) when ?IS_WS(C) ->
- error_terminate(400, State, {connection_error, protocol_error,
- 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2)'});
+parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, <<>>) when ?IS_WS(C) ->
+ error_terminate(400, State#state{in_state=PS#ps_header{headers=H}},
+ {connection_error, protocol_error,
+ 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2)'});
parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) when ?IS_WS(C) ->
parse_hd_name_ws(Rest, State, H, SoFar);
parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) ->
@@ -483,8 +485,9 @@ parse_hd_before_value(Buffer, State=#state{opts=Opts, in_state=PS}, H, N) ->
MaxLength = maps:get(max_header_value_length, Opts, 4096),
case match_eol(Buffer, 0) of
nomatch when byte_size(Buffer) > MaxLength ->
- error_terminate(431, State, {connection_error, limit_reached,
- 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
+ error_terminate(431, State#state{in_state=PS#ps_header{headers=H}},
+ {connection_error, limit_reached,
+ 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'});
nomatch ->
{more, State#state{in_state=PS#ps_header{headers=H, name=N}}, Buffer};
_ ->
@@ -538,12 +541,13 @@ horse_clean_value_ws_end() ->
-endif.
request(Buffer, State=#state{transport=Transport, in_streamid=StreamID,
- in_state=#ps_header{version=Version}}, Headers) ->
+ in_state=PS=#ps_header{version=Version}}, Headers) ->
case maps:get(<<"host">>, Headers, undefined) of
undefined when Version =:= 'HTTP/1.1' ->
%% @todo Might want to not close the connection on this and next one.
- error_terminate(400, State, {stream_error, StreamID, protocol_error,
- 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'});
+ error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}},
+ {stream_error, StreamID, protocol_error,
+ 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'});
undefined ->
request(Buffer, State, Headers, <<>>, default_port(Transport:secure()));
RawHost ->
@@ -553,8 +557,9 @@ request(Buffer, State=#state{transport=Transport, in_streamid=StreamID,
{Host, Port} ->
request(Buffer, State, Headers, Host, Port)
catch _:_ ->
- error_terminate(400, State, {stream_error, StreamID, protocol_error,
- 'The host header is invalid. (RFC7230 5.4)'})
+ error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}},
+ {stream_error, StreamID, protocol_error,
+ 'The host header is invalid. (RFC7230 5.4)'})
end
end.
@@ -565,7 +570,7 @@ default_port(_) -> 80.
%% End of request parsing.
request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_streamid=StreamID,
- in_state=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
+ in_state=PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
Headers, Host, Port) ->
Scheme = case Transport:secure() of
true -> <<"https">>;
@@ -578,9 +583,9 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream
Length = try
cow_http_hd:parse_content_length(BinLength)
catch _:_ ->
- error_terminate(400, State0, {stream_error, StreamID, protocol_error,
- 'The content-length header is invalid. (RFC7230 3.3.2)'})
- %% @todo Err should terminate here...
+ error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}},
+ {stream_error, StreamID, protocol_error,
+ 'The content-length header is invalid. (RFC7230 3.3.2)'})
end,
{true, Length, fun cow_http_te:stream_identity/2, {0, Length}};
%% @todo Better handling of transfer decoding.
@@ -622,7 +627,10 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream
end,
{request, Req, State, Buffer};
{true, HTTP2Settings} ->
- http2_upgrade(State0, Buffer, HTTP2Settings, Req)
+ %% We save the headers in case the upgrade will fail
+ %% and we need to pass them to cowboy_stream:early_error.
+ http2_upgrade(State0#state{in_state=PS#ps_header{headers=Headers}},
+ Buffer, HTTP2Settings, Req)
end.
%% HTTP/2 upgrade.
@@ -667,6 +675,8 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
%% Always half-closed stream coming from this side.
try cow_http_hd:parse_http2_settings(HTTP2Settings) of
Settings ->
+ %% @todo We should invoke cowboy_stream:info for this stream,
+ %% with a switch_protocol tuple.
Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(#{
<<"connection">> => <<"Upgrade">>,
<<"upgrade">> => <<"h2c">>
@@ -676,7 +686,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
catch _:_ ->
error_terminate(400, State, {connection_error, protocol_error,
- 'The HTTP2-Settings header contains a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})
+ 'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})
end.
%% Request body parsing.
@@ -916,6 +926,7 @@ stream_terminate(State=#state{socket=Socket, transport=Transport,
= lists:keytake(StreamID, #stream.id, Streams0),
_ = case OutState of
wait ->
+ %% @todo This should probably go through the stream handler info callback.
Transport:send(Socket, cow_http:response(204, 'HTTP/1.1', []));
chunked when Version =:= 'HTTP/1.1' ->
Transport:send(Socket, <<"0\r\n\r\n">>);
@@ -1005,11 +1016,36 @@ connection_hd_is_close(Conn) ->
Conns = cow_http_hd:parse_connection(iolist_to_binary(Conn)),
lists:member(<<"close">>, Conns).
+%% This function is only called when an error occurs on a new stream.
-spec error_terminate(cowboy:http_status(), #state{}, _) -> no_return().
-error_terminate(StatusCode, State=#state{socket=Socket, transport=Transport}, Reason) ->
- Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', [
- {<<"content-length">>, <<"0">>}
- ])),
+error_terminate(StatusCode0, State=#state{ref=Ref, socket=Socket, transport=Transport,
+ opts=Opts, peer=Peer, in_streamid=StreamID, in_state=StreamState}, Reason) ->
+ PartialReq = case StreamState of
+ #ps_request_line{} ->
+ #{};
+ #ps_header{method=Method, path=Path, qs=Qs,
+ version=Version, headers=ReqHeaders} -> #{
+ ref => Ref,
+ peer => Peer,
+ method => Method,
+ path => Path,
+ qs => Qs,
+ version => Version,
+ headers => case ReqHeaders of
+ undefined -> #{};
+ _ -> ReqHeaders
+ end
+ }
+ end,
+ {response, StatusCode, RespHeaders, RespBody}
+ = cowboy_stream:early_error(StreamID, Reason, PartialReq,
+ {response, StatusCode0, #{
+ <<"content-length">> => <<"0">>
+ }, <<>>}, Opts),
+ Transport:send(Socket, [
+ cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)),
+ RespBody
+ ]),
terminate(State, Reason).
-spec terminate(_, _) -> no_return().
diff --git a/src/cowboy_stream.erl b/src/cowboy_stream.erl
index 19b3663..d4ddc9f 100644
--- a/src/cowboy_stream.erl
+++ b/src/cowboy_stream.erl
@@ -25,7 +25,11 @@
%% @todo Perhaps it makes more sense to have resp_body in this module?
--type commands() :: [{response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()}
+-type resp_command()
+ :: {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()}.
+-export_type([resp_command/0]).
+
+-type commands() :: [resp_command()
| {headers, cowboy:http_status(), cowboy:http_headers()}
| {data, fin(), iodata()}
| {push, binary(), binary(), binary(), inet:port_number(),
@@ -51,10 +55,15 @@
| {stop, cow_http2:frame(), human_reason()}.
-export_type([reason/0]).
+-type partial_req() :: map(). %% @todo Take what's in cowboy_req with everything? optional.
+-export_type([partial_req/0]).
+
-callback init(streamid(), cowboy_req:req(), cowboy:opts()) -> {commands(), state()}.
-callback data(streamid(), fin(), binary(), State) -> {commands(), State} when State::state().
-callback info(streamid(), any(), State) -> {commands(), State} when State::state().
-callback terminate(streamid(), reason(), state()) -> any().
+-callback early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts())
+ -> Resp when Resp::resp_command().
%% @todo To optimize the number of active timers we could have a command
%% that enables a timeout that is called in the absence of any other call,
@@ -71,6 +80,7 @@
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
+-export([early_error/5]).
%% Note that this and other functions in this module do NOT catch
%% exceptions. We want the exception to go all the way down to the
@@ -127,3 +137,15 @@ terminate(_, _, undefined) ->
terminate(StreamID, Reason, {Handler, State}) ->
_ = Handler:terminate(StreamID, Reason, State),
ok.
+
+-spec early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts())
+ -> Resp when Resp::resp_command().
+early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
+ case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of
+ [] ->
+ Resp;
+ [Handler|Tail] ->
+ %% This is the same behavior as in init/3.
+ Handler:early_error(StreamID, Reason,
+ PartialReq, Resp, Opts#{stream_handlers => Tail})
+ end.
diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl
index 390f986..ec32ff0 100644
--- a/src/cowboy_stream_h.erl
+++ b/src/cowboy_stream_h.erl
@@ -19,6 +19,7 @@
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
+-export([early_error/5]).
-export([proc_lib_hack/3]).
-export([execute/3]).
@@ -122,6 +123,12 @@ info(_StreamID, _Info, State) ->
terminate(_StreamID, _Reason, _State) ->
ok.
+-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
+ cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
+ when Resp::cowboy_stream:resp_command().
+early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
+ cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
+
%% We use ~999999p here instead of ~w because the latter doesn't
%% support printable strings.
report_crash(_, _, _, normal, _) ->