mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-15 04:30:25 +00:00

This callback is called when an error occurs before the request (including headers, excluding body) was fully received. The init/3 callback will not be called. The callback receives the partial Req object (possibly empty), the reason for the error and the response command that the server will send. It allows you to be aware of the error and possibly modify the response before it is sent.
174 lines
6.3 KiB
Erlang
174 lines
6.3 KiB
Erlang
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
|
|
%%
|
|
%% Permission to use, copy, modify, and/or distribute this software for any
|
|
%% purpose with or without fee is hereby granted, provided that the above
|
|
%% copyright notice and this permission notice appear in all copies.
|
|
%%
|
|
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
-module(cowboy_compress_h).
|
|
-behavior(cowboy_stream).
|
|
|
|
-export([init/3]).
|
|
-export([data/4]).
|
|
-export([info/3]).
|
|
-export([terminate/3]).
|
|
-export([early_error/5]).
|
|
|
|
-record(state, {
|
|
next :: any(),
|
|
compress = undefined :: undefined | gzip,
|
|
deflate = undefined :: undefined | zlib:zstream()
|
|
}).
|
|
|
|
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
|
|
-> {cowboy_stream:commands(), #state{}}.
|
|
init(StreamID, Req, Opts) ->
|
|
State0 = check_req(Req),
|
|
{Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),
|
|
fold(Commands0, State0#state{next=Next}).
|
|
|
|
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
|
|
-> {cowboy_stream:commands(), State} when State::#state{}.
|
|
data(StreamID, IsFin, Data, State0=#state{next=Next0}) ->
|
|
{Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
|
|
fold(Commands0, State0#state{next=Next}).
|
|
|
|
-spec info(cowboy_stream:streamid(), any(), State)
|
|
-> {cowboy_stream:commands(), State} when State::#state{}.
|
|
info(StreamID, Info, State0=#state{next=Next0}) ->
|
|
{Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),
|
|
fold(Commands0, State0#state{next=Next}).
|
|
|
|
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
|
|
terminate(StreamID, Reason, #state{next=Next, deflate=Z}) ->
|
|
%% Clean the zlib:stream() in case something went wrong.
|
|
%% In the normal scenario the stream is already closed.
|
|
case Z of
|
|
undefined -> ok;
|
|
_ -> zlib:close(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.
|
|
check_req(Req) ->
|
|
case cowboy_req:parse_header(<<"accept-encoding">>, Req) of
|
|
%% Client doesn't support any compression algorithm.
|
|
undefined ->
|
|
#state{compress=undefined};
|
|
Encodings ->
|
|
%% We only support gzip so look for it specifically.
|
|
%% @todo A recipient SHOULD consider "x-gzip" to be
|
|
%% equivalent to "gzip". (RFC7230 4.2.3)
|
|
case [E || E={<<"gzip">>, Q} <- Encodings, Q =/= 0] of
|
|
[] ->
|
|
#state{compress=undefined};
|
|
_ ->
|
|
#state{compress=gzip}
|
|
end
|
|
end.
|
|
|
|
%% Do not compress responses that contain the content-encoding header.
|
|
check_resp_headers(#{<<"content-encoding">> := _}, State) ->
|
|
State#state{compress=undefined};
|
|
check_resp_headers(_, State) ->
|
|
State.
|
|
|
|
fold(Commands, State=#state{compress=undefined}) ->
|
|
{Commands, State};
|
|
fold(Commands, State) ->
|
|
fold(Commands, State, []).
|
|
|
|
fold([], State, Acc) ->
|
|
{lists:reverse(Acc), State};
|
|
%% We do not compress sendfile bodies.
|
|
fold([Response={response, _, _, {sendfile, _, _, _}}|Tail], State, Acc) ->
|
|
fold(Tail, State, [Response|Acc]);
|
|
%% We compress full responses directly, unless they are lower than
|
|
%% 300 bytes or we find we are not able to by looking at the headers.
|
|
%% @todo It might be good to allow this size to be configured?
|
|
fold([Response0={response, _, Headers, Body}|Tail], State0, Acc) ->
|
|
case check_resp_headers(Headers, State0) of
|
|
State=#state{compress=undefined} ->
|
|
fold(Tail, State, [Response0|Acc]);
|
|
State1 ->
|
|
BodyLength = iolist_size(Body),
|
|
if
|
|
BodyLength =< 300 ->
|
|
fold(Tail, State1, [Response0|Acc]);
|
|
true ->
|
|
{Response, State} = gzip_response(Response0, State1),
|
|
fold(Tail, State, [Response|Acc])
|
|
end
|
|
end;
|
|
%% Check headers and initiate compression...
|
|
fold([Response0={headers, _, Headers}|Tail], State0, Acc) ->
|
|
case check_resp_headers(Headers, State0) of
|
|
State=#state{compress=undefined} ->
|
|
fold(Tail, State, [Response0|Acc]);
|
|
State1 ->
|
|
{Response, State} = gzip_headers(Response0, State1),
|
|
fold(Tail, State, [Response|Acc])
|
|
end;
|
|
%% then compress each data commands individually.
|
|
fold([Data0={data, _, _}|Tail], State0=#state{compress=gzip}, Acc) ->
|
|
{Data, State} = gzip_data(Data0, State0),
|
|
fold(Tail, State, [Data|Acc]);
|
|
%% Otherwise, we either have an unrelated command, or a data command
|
|
%% with compression disabled.
|
|
fold([Command|Tail], State, Acc) ->
|
|
fold(Tail, State, [Command|Acc]).
|
|
|
|
gzip_response({response, Status, Headers, Body}, State) ->
|
|
%% We can't call zlib:gzip/1 because it does an
|
|
%% iolist_to_binary(GzBody) at the end to return
|
|
%% a binary(). Therefore the code here is largely
|
|
%% a duplicate of the code of that function.
|
|
Z = zlib:open(),
|
|
GzBody = try
|
|
%% 31 = 16+?MAX_WBITS from zlib.erl
|
|
%% @todo It might be good to allow them to be configured?
|
|
zlib:deflateInit(Z, default, deflated, 31, 8, default),
|
|
Gz = zlib:deflate(Z, Body, finish),
|
|
zlib:deflateEnd(Z),
|
|
Gz
|
|
after
|
|
zlib:close(Z)
|
|
end,
|
|
{{response, Status, Headers#{
|
|
<<"content-length">> => integer_to_binary(iolist_size(GzBody)),
|
|
<<"content-encoding">> => <<"gzip">>
|
|
}, GzBody}, State}.
|
|
|
|
gzip_headers({headers, Status, Headers0}, State) ->
|
|
Z = zlib:open(),
|
|
%% We use the same arguments as when compressing the body fully.
|
|
%% @todo It might be good to allow them to be configured?
|
|
zlib:deflateInit(Z, default, deflated, 31, 8, default),
|
|
Headers = maps:remove(<<"content-length">>, Headers0),
|
|
{{headers, Status, Headers#{
|
|
<<"content-encoding">> => <<"gzip">>
|
|
}}, State#state{deflate=Z}}.
|
|
|
|
gzip_data({data, nofin, Data0}, State=#state{deflate=Z}) ->
|
|
Data = zlib:deflate(Z, Data0),
|
|
{{data, nofin, Data}, State};
|
|
gzip_data({data, fin, Data0}, State=#state{deflate=Z}) ->
|
|
Data = zlib:deflate(Z, Data0, finish),
|
|
zlib:deflateEnd(Z),
|
|
zlib:close(Z),
|
|
{{data, fin, Data}, State#state{deflate=undefined}}.
|