0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Add optional automatic response body compression

This behavior can be enabled with the `compress` protocol option.
See the `compress_response` example for more details.

All tests are now ran with and without compression for both HTTP
and HTTPS.
This commit is contained in:
Loïc Hoguin 2013-01-07 22:42:16 +01:00
parent a013becc66
commit 01f57ad65d
13 changed files with 294 additions and 23 deletions

View file

@ -67,7 +67,7 @@ autobahn:
build-plt: app build-plt: app
@dialyzer --build_plt --output_plt .$(PROJECT).plt \ @dialyzer --build_plt --output_plt .$(PROJECT).plt \
--apps kernel stdlib sasl inets crypto public_key ssl deps/ranch --apps erts kernel stdlib sasl inets crypto public_key ssl deps/ranch
dialyze: dialyze:
@dialyzer --src src --plt .$(PROJECT).plt --no_native \ @dialyzer --src src --plt .$(PROJECT).plt --no_native \

View file

@ -4,6 +4,9 @@ Cowboy Examples
* [chunked_hello_world](./examples/chunked_hello_world): * [chunked_hello_world](./examples/chunked_hello_world):
demonstrates chunked data transfer with two one-second delays demonstrates chunked data transfer with two one-second delays
* [compress_response](./examples/compress_response)
send a response body compressed if the client supports it
* [cookie](./examples/cookie): * [cookie](./examples/cookie):
set cookies from server and client side set cookies from server and client side

View file

@ -0,0 +1,62 @@
Cowboy Compress Response
========================
To compile this example you need rebar in your PATH.
Type the following command:
```
$ rebar get-deps compile
```
You can then start the Erlang node with the following command:
```
./start.sh
```
Then point your browser to the indicated URL.
Example
-------
``` bash
$ curl -i http://localhost:8080
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Mon, 07 Jan 2013 18:42:29 GMT
content-length: 909
A cowboy is an animal herder who tends cattle on ranches in North America,
traditionally on horseback, and often performs a multitude of other ranch-
related tasks. The historic American cowboy of the late 19th century arose
from the vaquero traditions of northern Mexico and became a figure of special
significance and legend. A subtype, called a wrangler, specifically tends the
horses used to work cattle. In addition to ranch work, some cowboys work for
or participate in rodeos. Cowgirls, first defined as such in the late 19th
century, had a less-well documented historical role, but in the modern world
have established the ability to work at virtually identical tasks and obtained
considerable respect for their achievements. There are also cattle handlers
in many other parts of the world, particularly South America and Australia,
who perform work similar to the cowboy in their respective nations.
$ curl -i --compressed http://localhost:8080
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Mon, 07 Jan 2013 18:42:30 GMT
content-encoding: gzip
content-length: 510
A cowboy is an animal herder who tends cattle on ranches in North America,
traditionally on horseback, and often performs a multitude of other ranch-
related tasks. The historic American cowboy of the late 19th century arose
from the vaquero traditions of northern Mexico and became a figure of special
significance and legend. A subtype, called a wrangler, specifically tends the
horses used to work cattle. In addition to ranch work, some cowboys work for
or participate in rodeos. Cowgirls, first defined as such in the late 19th
century, had a less-well documented historical role, but in the modern world
have established the ability to work at virtually identical tasks and obtained
considerable respect for their achievements. There are also cattle handlers
in many other parts of the world, particularly South America and Australia,
who perform work similar to the cowboy in their respective nations.
```

View file

@ -0,0 +1,4 @@
{deps, [
{cowboy, ".*",
{git, "git://github.com/extend/cowboy.git", "master"}}
]}.

View file

@ -0,0 +1,15 @@
%% Feel free to use, reuse and abuse the code in this file.
{application, compress_response, [
{description, "Cowboy Compress Response example."},
{vsn, "1"},
{modules, []},
{registered, []},
{applications, [
kernel,
stdlib,
cowboy
]},
{mod, {compress_response_app, []}},
{env, []}
]}.

View file

@ -0,0 +1,14 @@
%% Feel free to use, reuse and abuse the code in this file.
-module(compress_response).
%% API.
-export([start/0]).
%% API.
start() ->
ok = application:start(crypto),
ok = application:start(ranch),
ok = application:start(cowboy),
ok = application:start(compress_response).

View file

@ -0,0 +1,26 @@
%% Feel free to use, reuse and abuse the code in this file.
%% @private
-module(compress_response_app).
-behaviour(application).
%% API.
-export([start/2]).
-export([stop/1]).
%% API.
start(_Type, _Args) ->
Dispatch = [
{'_', [
{[], toppage_handler, []}
]}
],
{ok, _} = cowboy:start_http(http, 100, [{port, 8080}], [
{compress, true},
{env, [{dispatch, Dispatch}]}
]),
compress_response_sup:start_link().
stop(_State) ->
ok.

View file

@ -0,0 +1,23 @@
%% Feel free to use, reuse and abuse the code in this file.
%% @private
-module(compress_response_sup).
-behaviour(supervisor).
%% API.
-export([start_link/0]).
%% supervisor.
-export([init/1]).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% supervisor.
init([]) ->
Procs = [],
{ok, {{one_for_one, 10, 10}, Procs}}.

View file

@ -0,0 +1,31 @@
%% Feel free to use, reuse and abuse the code in this file.
%% @doc Compress response handler.
-module(toppage_handler).
-export([init/3]).
-export([handle/2]).
-export([terminate/2]).
init(_Transport, Req, []) ->
{ok, Req, undefined}.
handle(Req, State) ->
BigBody =
<<"A cowboy is an animal herder who tends cattle on ranches in North America,
traditionally on horseback, and often performs a multitude of other ranch-
related tasks. The historic American cowboy of the late 19th century arose
from the vaquero traditions of northern Mexico and became a figure of special
significance and legend. A subtype, called a wrangler, specifically tends the
horses used to work cattle. In addition to ranch work, some cowboys work for
or participate in rodeos. Cowgirls, first defined as such in the late 19th
century, had a less-well documented historical role, but in the modern world
have established the ability to work at virtually identical tasks and obtained
considerable respect for their achievements. There are also cattle handlers
in many other parts of the world, particularly South America and Australia,
who perform work similar to the cowboy in their respective nations.\n">>,
{ok, Req2} = cowboy_req:reply(200, [], BigBody, Req),
{ok, Req2, State}.
terminate(_Req, _State) ->
ok.

View file

@ -0,0 +1,3 @@
#!/bin/sh
erl -pa ebin deps/*/ebin -s compress_response \
-eval "io:format(\"Point your browser at http://localhost:8080~n\")."

View file

@ -17,6 +17,8 @@
%% %%
%% The available options are: %% The available options are:
%% <dl> %% <dl>
%% <dt>compress</dt><dd>Whether to automatically compress the response
%% body when the conditions are met. Disabled by default.</dd>
%% <dt>env</dt><dd>The environment passed and optionally modified %% <dt>env</dt><dd>The environment passed and optionally modified
%% by middlewares.</dd> %% by middlewares.</dd>
%% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request. %% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
@ -64,6 +66,7 @@
socket :: inet:socket(), socket :: inet:socket(),
transport :: module(), transport :: module(),
middlewares :: [module()], middlewares :: [module()],
compress :: boolean(),
env :: cowboy_middleware:env(), env :: cowboy_middleware:env(),
onrequest :: undefined | onrequest_fun(), onrequest :: undefined | onrequest_fun(),
onresponse = undefined :: undefined | onresponse_fun(), onresponse = undefined :: undefined | onresponse_fun(),
@ -99,6 +102,7 @@ get_value(Key, Opts, Default) ->
%% @private %% @private
-spec init(pid(), inet:socket(), module(), any()) -> ok. -spec init(pid(), inet:socket(), module(), any()) -> ok.
init(ListenerPid, Socket, Transport, Opts) -> init(ListenerPid, Socket, Transport, Opts) ->
Compress = get_value(compress, Opts, false),
MaxEmptyLines = get_value(max_empty_lines, Opts, 5), MaxEmptyLines = get_value(max_empty_lines, Opts, 5),
MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64), MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64),
MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096), MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096),
@ -112,7 +116,7 @@ init(ListenerPid, Socket, Transport, Opts) ->
Timeout = get_value(timeout, Opts, 5000), Timeout = get_value(timeout, Opts, 5000),
ok = ranch:accept_ack(ListenerPid), ok = ranch:accept_ack(ListenerPid),
wait_request(<<>>, #state{socket=Socket, transport=Transport, wait_request(<<>>, #state{socket=Socket, transport=Transport,
middlewares=Middlewares, env=Env, middlewares=Middlewares, compress=Compress, env=Env,
max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive, max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive,
max_request_line_length=MaxRequestLineLength, max_request_line_length=MaxRequestLineLength,
max_header_name_length=MaxHeaderNameLength, max_header_name_length=MaxHeaderNameLength,
@ -457,11 +461,11 @@ parse_host(<< C, Rest/bits >>, Acc) ->
request(Buffer, State=#state{socket=Socket, transport=Transport, request(Buffer, State=#state{socket=Socket, transport=Transport,
req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive, req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive,
onresponse=OnResponse}, compress=Compress, onresponse=OnResponse},
Method, Path, Query, Fragment, Version, Headers, Host, Port) -> Method, Path, Query, Fragment, Version, Headers, Host, Port) ->
Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment, Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive, Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive,
OnResponse), Compress, OnResponse),
onrequest(Req, State). onrequest(Req, State).
%% Call the global onrequest callback. The callback can send a reply, %% Call the global onrequest callback. The callback can send a reply,
@ -546,13 +550,13 @@ error_terminate(Code, Req, State) ->
%% Only send an error reply if there is no resp_sent message. %% Only send an error reply if there is no resp_sent message.
-spec error_terminate(cowboy_http:status(), #state{}) -> ok. -spec error_terminate(cowboy_http:status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport, error_terminate(Code, State=#state{socket=Socket, transport=Transport,
onresponse=OnResponse}) -> compress=Compress, onresponse=OnResponse}) ->
receive receive
{cowboy_req, resp_sent} -> ok {cowboy_req, resp_sent} -> ok
after 0 -> after 0 ->
_ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport, _ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport,
<<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined, <<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined,
<<>>, false, OnResponse)), <<>>, false, Compress, OnResponse)),
ok ok
end, end,
terminate(State). terminate(State).

View file

@ -42,7 +42,7 @@
-module(cowboy_req). -module(cowboy_req).
%% Request API. %% Request API.
-export([new/13]). -export([new/14]).
-export([method/1]). -export([method/1]).
-export([version/1]). -export([version/1]).
-export([peer/1]). -export([peer/1]).
@ -156,6 +156,7 @@
buffer = <<>> :: binary(), buffer = <<>> :: binary(),
%% Response. %% Response.
resp_compress = false :: boolean(),
resp_state = waiting :: locked | waiting | chunks | done, resp_state = waiting :: locked | waiting | chunks | done,
resp_headers = [] :: cowboy_http:headers(), resp_headers = [] :: cowboy_http:headers(),
resp_body = <<>> :: iodata() | resp_body_fun() resp_body = <<>> :: iodata() | resp_body_fun()
@ -179,16 +180,16 @@
%% in an optimized way and add the parsed value to p_headers' cache. %% in an optimized way and add the parsed value to p_headers' cache.
-spec new(inet:socket(), module(), binary(), binary(), binary(), binary(), -spec new(inet:socket(), module(), binary(), binary(), binary(), binary(),
cowboy_http:version(), cowboy_http:headers(), binary(), cowboy_http:version(), cowboy_http:headers(), binary(),
inet:port_number() | undefined, binary(), boolean(), inet:port_number() | undefined, binary(), boolean(), boolean(),
undefined | cowboy_protocol:onresponse_fun()) undefined | cowboy_protocol:onresponse_fun())
-> req(). -> req().
new(Socket, Transport, Method, Path, Query, Fragment, new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, CanKeepalive, Version, Headers, Host, Port, Buffer, CanKeepalive,
OnResponse) -> Compress, OnResponse) ->
Req = #http_req{socket=Socket, transport=Transport, pid=self(), Req = #http_req{socket=Socket, transport=Transport, pid=self(),
method=Method, path=Path, qs=Query, fragment=Fragment, version=Version, method=Method, path=Path, qs=Query, fragment=Fragment, version=Version,
headers=Headers, host=Host, port=Port, buffer=Buffer, headers=Headers, host=Host, port=Port, buffer=Buffer,
onresponse=OnResponse}, resp_compress=Compress, onresponse=OnResponse},
case CanKeepalive and (Version =:= {1, 1}) of case CanKeepalive and (Version =:= {1, 1}) of
false -> false ->
Req#http_req{connection=close}; Req#http_req{connection=close};
@ -892,7 +893,8 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
reply(Status, Headers, Body, Req=#http_req{ reply(Status, Headers, Body, Req=#http_req{
socket=Socket, transport=Transport, socket=Socket, transport=Transport,
version=Version, connection=Connection, version=Version, connection=Connection,
method=Method, resp_state=waiting, resp_headers=RespHeaders}) -> method=Method, resp_compress=Compress,
resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection), RespConn = response_connection(Headers, Connection),
HTTP11Headers = case Version of HTTP11Headers = case Version of
{1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}]; {1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}];
@ -922,18 +924,60 @@ reply(Status, Headers, Body, Req=#http_req{
BodyFun(Socket, Transport); BodyFun(Socket, Transport);
true -> ok true -> ok
end; end;
_ when Compress ->
Req2 = reply_may_compress(Status, Headers, Body, Req,
RespHeaders, HTTP11Headers, Method);
_ -> _ ->
{_, Req2} = response(Status, Headers, RespHeaders, [ Req2 = reply_no_compress(Status, Headers, Body, Req,
{<<"content-length">>, integer_to_list(iolist_size(Body))}, RespHeaders, HTTP11Headers, Method, iolist_size(Body))
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers],
case Method of <<"HEAD">> -> <<>>; _ -> Body end,
Req)
end, end,
{ok, Req2#http_req{connection=RespConn, resp_state=done, {ok, Req2#http_req{connection=RespConn, resp_state=done,
resp_headers=[], resp_body= <<>>}}. resp_headers=[], resp_body= <<>>}}.
reply_may_compress(Status, Headers, Body, Req,
RespHeaders, HTTP11Headers, Method) ->
BodySize = iolist_size(Body),
{ok, Encodings, Req2}
= cowboy_req:parse_header(<<"accept-encoding">>, Req),
CanGzip = (BodySize > 300)
andalso (false =:= lists:keyfind(<<"content-encoding">>,
1, Headers))
andalso (false =:= lists:keyfind(<<"content-encoding">>,
1, RespHeaders))
andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
1, Headers))
andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
1, RespHeaders))
andalso (Encodings =/= undefined)
andalso (false =/= lists:keyfind(<<"gzip">>, 1, Encodings)),
case CanGzip of
true ->
GzBody = zlib:gzip(Body),
{_, Req3} = response(Status, Headers, RespHeaders, [
{<<"content-length">>, integer_to_list(byte_size(GzBody))},
{<<"content-encoding">>, <<"gzip">>},
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers],
case Method of <<"HEAD">> -> <<>>; _ -> GzBody end,
Req2),
Req3;
false ->
reply_no_compress(Status, Headers, Body, Req,
RespHeaders, HTTP11Headers, Method, BodySize)
end.
reply_no_compress(Status, Headers, Body, Req,
RespHeaders, HTTP11Headers, Method, BodySize) ->
{_, Req2} = response(Status, Headers, RespHeaders, [
{<<"content-length">>, integer_to_list(BodySize)},
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers],
case Method of <<"HEAD">> -> <<>>; _ -> Body end,
Req),
Req2.
%% @equiv chunked_reply(Status, [], Req) %% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req(). -spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req().
chunked_reply(Status, Req) -> chunked_reply(Status, Req) ->

View file

@ -78,7 +78,14 @@
%% ct. %% ct.
all() -> all() ->
[{group, http}, {group, https}, {group, onrequest}, {group, onresponse}]. [
{group, http},
{group, https},
{group, http_compress},
{group, https_compress},
{group, onrequest},
{group, onresponse}
].
groups() -> groups() ->
Tests = [ Tests = [
@ -130,6 +137,8 @@ groups() ->
[ [
{http, [], Tests}, {http, [], Tests},
{https, [], Tests}, {https, [], Tests},
{http_compress, [], Tests},
{https_compress, [], Tests},
{onrequest, [], [ {onrequest, [], [
onrequest, onrequest,
onrequest_reply onrequest_reply
@ -185,9 +194,42 @@ init_per_group(https, Config) ->
{ok, Client} = cowboy_client:init(Opts), {ok, Client} = cowboy_client:init(Opts),
[{scheme, <<"https">>}, {port, Port}, {opts, Opts}, [{scheme, <<"https">>}, {port, Port}, {opts, Opts},
{transport, Transport}, {client, Client}|Config1]; {transport, Transport}, {client, Client}|Config1];
init_per_group(onrequest, Config) -> init_per_group(http_compress, Config) ->
Port = 33082, Port = 33082,
Transport = ranch_tcp, Transport = ranch_tcp,
Config1 = init_static_dir(Config),
{ok, _} = cowboy:start_http(http_compress, 100, [{port, Port}], [
{compress, true},
{env, [{dispatch, init_dispatch(Config1)}]},
{max_keepalive, 50},
{timeout, 500}
]),
{ok, Client} = cowboy_client:init([]),
[{scheme, <<"http">>}, {port, Port}, {opts, []},
{transport, Transport}, {client, Client}|Config1];
init_per_group(https_compress, Config) ->
Port = 33083,
Transport = ranch_ssl,
Opts = [
{certfile, ?config(data_dir, Config) ++ "cert.pem"},
{keyfile, ?config(data_dir, Config) ++ "key.pem"},
{password, "cowboy"}
],
Config1 = init_static_dir(Config),
application:start(public_key),
application:start(ssl),
{ok, _} = cowboy:start_https(https_compress, 100, Opts ++ [{port, Port}], [
{compress, true},
{env, [{dispatch, init_dispatch(Config1)}]},
{max_keepalive, 50},
{timeout, 500}
]),
{ok, Client} = cowboy_client:init(Opts),
[{scheme, <<"https">>}, {port, Port}, {opts, Opts},
{transport, Transport}, {client, Client}|Config1];
init_per_group(onrequest, Config) ->
Port = 33084,
Transport = ranch_tcp,
{ok, _} = cowboy:start_http(onrequest, 100, [{port, Port}], [ {ok, _} = cowboy:start_http(onrequest, 100, [{port, Port}], [
{env, [{dispatch, init_dispatch(Config)}]}, {env, [{dispatch, init_dispatch(Config)}]},
{max_keepalive, 50}, {max_keepalive, 50},
@ -198,7 +240,7 @@ init_per_group(onrequest, Config) ->
[{scheme, <<"http">>}, {port, Port}, {opts, []}, [{scheme, <<"http">>}, {port, Port}, {opts, []},
{transport, Transport}, {client, Client}|Config]; {transport, Transport}, {client, Client}|Config];
init_per_group(onresponse, Config) -> init_per_group(onresponse, Config) ->
Port = 33083, Port = 33085,
Transport = ranch_tcp, Transport = ranch_tcp,
{ok, _} = cowboy:start_http(onresponse, 100, [{port, Port}], [ {ok, _} = cowboy:start_http(onresponse, 100, [{port, Port}], [
{env, [{dispatch, init_dispatch(Config)}]}, {env, [{dispatch, init_dispatch(Config)}]},
@ -210,13 +252,13 @@ init_per_group(onresponse, Config) ->
[{scheme, <<"http">>}, {port, Port}, {opts, []}, [{scheme, <<"http">>}, {port, Port}, {opts, []},
{transport, Transport}, {client, Client}|Config]. {transport, Transport}, {client, Client}|Config].
end_per_group(https, Config) -> end_per_group(Group, Config) when Group =:= https; Group =:= https_compress ->
cowboy:stop_listener(https), cowboy:stop_listener(https),
application:stop(ssl), application:stop(ssl),
application:stop(public_key), application:stop(public_key),
end_static_dir(Config), end_static_dir(Config),
ok; ok;
end_per_group(http, Config) -> end_per_group(Group, Config) when Group =:= http; Group =:= http_compress ->
cowboy:stop_listener(http), cowboy:stop_listener(http),
end_static_dir(Config); end_static_dir(Config);
end_per_group(Name, _) -> end_per_group(Name, _) ->