diff --git a/Makefile b/Makefile
index b471713..4db2e67 100644
--- a/Makefile
+++ b/Makefile
@@ -2,4 +2,4 @@ compile: ; rebar3 do compile, xref
eunit: ; rebar3 eunit
init_dialyzer: ; rebar3 dialyzer -s false
dialyzer: ; rebar3 dialyzer -u false
-travis: ; rebar3 do xref, dialyzer, eunit
+travis: ; rebar3 do xref, dialyzer, eunit && rebar3 coveralls send
diff --git a/README.md b/README.md
index 2a8fec5..baab683 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,10 @@
# elli - Erlang web server for HTTP APIs
-[![Travis CI][travis badge]][travis builds]
[![Hex.pm][hex badge]][hex package]
-[![Erlang][erlang badge]][erlang downloads]
[![Documentation][doc badge]][docs]
+[![Erlang][erlang badge]][erlang downloads]
+[![Travis CI][travis badge]][travis builds]
+[![Coverage Status][coveralls badge]][coveralls link]
[![MIT License][license badge]](LICENSE)
[travis builds]: https://travis-ci.org/elli-lib/elli
@@ -15,6 +16,8 @@
[erlang downloads]: http://www.erlang.org/downloads
[doc badge]: https://img.shields.io/badge/docs-edown-green.svg
[docs]: doc/README.md
+[coveralls badge]: https://coveralls.io/repos/github/elli-lib/elli/badge.svg?branch=develop
+[coveralls link]: https://coveralls.io/github/elli-lib/elli?branch=develop
[license badge]: https://img.shields.io/badge/license-MIT-blue.svg
Elli is a webserver you can run inside your Erlang application to
diff --git a/doc/README.md b/doc/README.md
index 1a00896..05af46e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -159,6 +159,7 @@ about benchmarking HTTP servers.
+adder |
elli |
elli_example_callback |
elli_example_callback_handover |
diff --git a/doc/edoc-info b/doc/edoc-info
index 7dfc7ca..98f05d9 100644
--- a/doc/edoc-info
+++ b/doc/edoc-info
@@ -1,5 +1,5 @@
%% encoding: UTF-8
{application,elli}.
-{modules,[elli,elli_example_callback,elli_example_callback_handover,
+{modules,[adder,elli,elli_example_callback,elli_example_callback_handover,
elli_handler,elli_http,elli_middleware,elli_middleware_compress,
elli_request,elli_tcp,elli_test,elli_util]}.
diff --git a/doc/elli_handler.md b/doc/elli_handler.md
index 2662af9..5ba6bc5 100644
--- a/doc/elli_handler.md
+++ b/doc/elli_handler.md
@@ -53,6 +53,6 @@ See [`elli_example_callback:handle_event/3`](elli_example_callback.md#handle_eve
-result() = {elli:response_code() | ok, elli:body()} | {elli:response_code() | ok, elli:headers(), elli:body()} | ignore
+result() = {elli:response_code() | ok, elli:headers(), {file, file:name_all()} | {file, file:name_all(), elli_util:range()}} | {elli:response_code() | ok, elli:headers(), elli:body()} | {elli:response_code() | ok, elli:body()} | {chunk, elli:headers()} | {chunk, elli:headers(), elli:body()} | ignore
diff --git a/doc/elli_test.md b/doc/elli_test.md
index aa893f7..12f4df9 100644
--- a/doc/elli_test.md
+++ b/doc/elli_test.md
@@ -32,7 +32,7 @@ The unit tests below test `elli_example_callback`.
### call/5 ###
-call(Method, Path, Headers, Body, Opts) -> elli:req()
+call(Method, Path, Headers, Body, Opts) -> elli_handler:result()
diff --git a/doc/elli_util.md b/doc/elli_util.md
index e4ce653..de10432 100644
--- a/doc/elli_util.md
+++ b/doc/elli_util.md
@@ -49,10 +49,10 @@ Encode Range to a Content-Range value.
### file_size/1 ###
-file_size(Filename::file:name()) -> non_neg_integer() | {error, Reason}
+file_size(Filename) -> Size | {error, Reason}
-
+
Get the size in bytes of the file.
diff --git a/rebar.config b/rebar.config
index 88e6072..949de8a 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,6 +1,5 @@
{erl_first_files, ["src/elli_handler.erl"]}.
{erl_opts, [debug_info, {i, "include"}]}.
-{cover_enabled, true}.
{deps, []}.
{xref_checks, [undefined_function_calls,locals_not_used]}.
{profiles, [
@@ -12,7 +11,25 @@
]},
{doclet, edown_doclet}
]}
+ ]},
+ {test, [
+ {deps, [{hackney, "1.6.3"}]}
]}
]}.
{shell, [{script_file, "bin/shell.escript"}]}.
+
+{project_plugins, [
+ {coveralls, "1.3.0"},
+ {rebar3_lint, "0.1.7"}
+]}.
+
+{provider_hooks, [{pre, [{eunit, lint}]}]}.
+
+{cover_enabled, true}.
+{cover_export_enabled, true}.
+{cover_excl_mods, [
+ elli_handler
+]}.
+{coveralls_coverdata, "_build/test/cover/eunit.coverdata"}.
+{coveralls_service_name, "travis-ci"}.
diff --git a/rebar.config.script b/rebar.config.script
index 03e3581..1411e37 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -1,12 +1,7 @@
-case erl_internal:bif(is_map, 1) of
- false -> CONFIG;
- true ->
- Lint = {rebar3_lint,
- {git, "git://github.com/project-fifo/rebar3_lint.git",
- {tag, "0.1.6"}}},
- Config1 = lists:keystore(project_plugins, 1, CONFIG,
- {project_plugins, [Lint]}),
- Config2 = lists:keystore(provider_hooks, 1, Config1,
- {provider_hooks, [{pre, [{eunit, lint}]}]})
-
+case os:getenv("TRAVIS") of
+ "true" ->
+ JobId = os:getenv("TRAVIS_JOB_ID"),
+ lists:keystore(coveralls_service_job_id, 1, CONFIG,
+ {coveralls_service_job_id, JobId});
+ _ -> CONFIG
end.
diff --git a/src/elli.app.src b/src/elli.app.src
index 7b54789..4786b15 100644
--- a/src/elli.app.src
+++ b/src/elli.app.src
@@ -1,7 +1,7 @@
{application, elli,
[
{description, "Erlang web server for HTTP APIs"},
- {vsn, "2.0.0"},
+ {vsn, "2.0.1"},
{modules, [
elli,
elli_example_callback,
@@ -24,7 +24,7 @@
]},
{env, []},
- {maintainers,["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]},
- {licenses,["MIT"]},
- {links,[{"Github","https://github.com/elli-lib/elli"}]}
+ {maintainers, ["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]},
+ {licenses, ["MIT"]},
+ {links, [{"GitHub", "https://github.com/elli-lib/elli"}]}
]}.
diff --git a/src/elli_example_callback.erl b/src/elli_example_callback.erl
index f0333e6..6677064 100644
--- a/src/elli_example_callback.erl
+++ b/src/elli_example_callback.erl
@@ -181,6 +181,9 @@ handle('GET', [<<"chunked">>], Req) ->
handle('GET', [<<"shorthand">>], _Req) ->
{200, <<"hello">>};
+handle('GET', [<<"ip">>], Req) ->
+ {<<"200 OK">>, elli_request:peer(Req)};
+
handle('GET', [<<"304">>], _Req) ->
%% A "Not Modified" response is exactly like a normal response (so
%% Content-Length is included), but the body will not be sent.
diff --git a/src/elli_handler.erl b/src/elli_handler.erl
index 6d2de3c..9342459 100644
--- a/src/elli_handler.erl
+++ b/src/elli_handler.erl
@@ -22,8 +22,14 @@
| client_closed | client_timeout
| invalid_return.
--type result() :: {elli:response_code() | ok, elli:body()}
+-type result() :: {elli:response_code() | ok,
+ elli:headers(),
+ {file, file:name_all()}
+ | {file, file:name_all(), elli_util:range()}}
| {elli:response_code() | ok, elli:headers(), elli:body()}
+ | {elli:response_code() | ok, elli:body()}
+ | {chunk, elli:headers()}
+ | {chunk, elli:headers(), elli:body()}
| ignore.
-callback handle(Req :: elli:req(), callback_args()) -> result().
diff --git a/src/elli_http.erl b/src/elli_http.erl
index 8b5caca..a58c1f1 100644
--- a/src/elli_http.erl
+++ b/src/elli_http.erl
@@ -70,7 +70,7 @@ keepalive_loop(Socket, Options, Callback) ->
keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) ->
case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of
{keep_alive, NewBuffer} ->
- ?MODULE:keepalive_loop(Socket, NumRequests,
+ ?MODULE:keepalive_loop(Socket, NumRequests + 1,
NewBuffer, Options, Callback);
{close, _} ->
elli_tcp:close(Socket),
@@ -87,7 +87,6 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) ->
Callback :: elli_handler:callback(),
ConnToken :: {'keep_alive' | 'close', binary()}.
handle_request(S, PrevB, Opts, {Mod, Args} = Callback) ->
- t(request_start),
{Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback),
t(headers_start),
{RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback),
@@ -280,10 +279,7 @@ send_server_error(Socket) ->
send_rescue_response(Socket, 500, <<"Server Error">>).
send_rescue_response(Socket, Code, Body) ->
- Response = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>,
- <<"Content-Length: ">>, integer_to_list(size(Body)), <<"\r\n">>,
- <<"\r\n">>,
- Body],
+ Response = http_response(Code, Body),
elli_tcp:send(Socket, Response).
%% @doc Execute the user callback, translating failure into a proper response.
@@ -407,23 +403,17 @@ send_chunk(Socket, Data) ->
%%
%% @doc Retrieve the request line.
-get_request(Socket, Buffer, Options, {Mod, Args} = Callback) ->
+get_request(Socket, <<>>, Options, Callback) ->
+ NewBuffer = recv_request(Socket, <<>>, Options, Callback),
+ get_request(Socket, NewBuffer, Options, Callback);
+get_request(Socket, Buffer, Options, Callback) ->
+ t(request_start),
+ get_request_(Socket, Buffer, Options, Callback).
+
+get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{more, _} ->
- case elli_tcp:recv(Socket, 0, request_timeout(Options)) of
- {ok, Data} ->
- NewBuffer = <>,
- get_request(Socket, NewBuffer, Options, Callback);
- {error, timeout} ->
- handle_event(Mod, request_timeout, [], Args),
- elli_tcp:close(Socket),
- exit(normal);
- {error, Closed} when Closed =:= closed orelse
- Closed =:= enotconn ->
- handle_event(Mod, request_closed, [], Args),
- elli_tcp:close(Socket),
- exit(normal)
- end;
+ recv_request(Socket, Buffer, Options, Callback);
{ok, {http_request, Method, RawPath, Version}, Rest} ->
{Method, RawPath, Version, Rest};
{ok, {http_error, _}, _} ->
@@ -436,6 +426,21 @@ get_request(Socket, Buffer, Options, {Mod, Args} = Callback) ->
exit(normal)
end.
+recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) ->
+ case elli_tcp:recv(Socket, 0, request_timeout(Options)) of
+ {ok, Data} ->
+ <>;
+ {error, timeout} ->
+ handle_event(Mod, request_timeout, [], Args),
+ elli_tcp:close(Socket),
+ exit(normal);
+ {error, Closed} when Closed =:= closed orelse
+ Closed =:= enotconn ->
+ handle_event(Mod, request_closed, [], Args),
+ elli_tcp:close(Socket),
+ exit(normal)
+ end.
+
-spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when
Socket :: elli_tcp:socket(),
V :: version(),
@@ -544,8 +549,7 @@ maybe_send_continue(Socket, Headers) ->
% headers contains "Expect:100-continue"
case proplists:get_value(<<"Expect">>, Headers, undefined) of
<<"100-continue">> ->
- Response = [<<"HTTP/1.1 ">>, status(100), <<"\r\n">>,
- <<"Content-Length: 0">>, <<"\r\n\r\n">>],
+ Response = http_response(100),
elli_tcp:send(Socket, Response);
_Other ->
ok
@@ -561,19 +565,18 @@ check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) ->
do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args})
when ContentLength > MaxSize ->
handle_event(Mod, bad_request, [{body_size, ContentLength}], Args),
- do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize),
+ do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize),
elli_tcp:close(Socket),
exit(normal);
do_check_max_size(_, _, _, _, _) -> ok.
-do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize)
+do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize)
when ContentLength < MaxSize * 2 ->
OnSocket = ContentLength - size(Buffer),
elli_tcp:recv(Socket, OnSocket, 60000),
- Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>,
- <<"Content-Length: 0">>, <<"\r\n\r\n">>],
+ Response = http_response(413),
elli_tcp:send(Socket, Response);
-do_check_max_size_2x(_, _, _, _) -> ok.
+do_check_max_size_x2(_, _, _, _) -> ok.
-spec mk_req(Method, PathTuple, Headers, Body, V, Socket, Callback) -> Req when
Method :: elli:http_method(),
@@ -603,9 +606,20 @@ mk_req(Method, RawPath, Headers, Body, V, Socket, {Mod, Args} = Callback) ->
%% HEADERS
%%
+http_response(Code) ->
+ http_response(Code, <<>>).
+
+http_response(Code, Body) ->
+ http_response(Code, [{<<"Content-Length">>, size(Body)}], Body).
+
+http_response(Code, Headers, <<>>) ->
+ [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>,
+ encode_headers(Headers), <<"\r\n">>];
+http_response(Code, Headers, Body) ->
+ [http_response(Code, Headers, <<>>), Body].
+
assemble_response_headers(Code, Headers) ->
- ResponseHeaders = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>,
- encode_headers(Headers), <<"\r\n">>],
+ ResponseHeaders = http_response(Code, Headers, <<>>),
s(resp_headers, iolist_size(ResponseHeaders)),
ResponseHeaders.
@@ -849,6 +863,6 @@ get_body_test() ->
Buffer = binary:copy(<<".">>, 42),
Opts = [],
Callback = {no, op},
- ?assertEqual({Buffer, <<>>},
+ ?assertMatch({Buffer, <<>>},
get_body(Socket, Headers, Buffer, Opts, Callback)).
-endif.
diff --git a/src/elli_test.erl b/src/elli_test.erl
index 7b7298a..8ef375a 100644
--- a/src/elli_test.erl
+++ b/src/elli_test.erl
@@ -12,7 +12,7 @@
-export([call/5]).
--spec call(Method, Path, Headers, Body, Opts) -> elli:req() when
+-spec call(Method, Path, Headers, Body, Opts) -> elli_handler:result() when
Method :: elli:http_method(),
Path :: binary(),
Headers :: elli:headers(),
@@ -30,13 +30,13 @@ call(Method, Path, Headers, Body, Opts) ->
-include_lib("eunit/include/eunit.hrl").
hello_world_test() ->
- ?assertEqual({ok, [], <<"Hello World!">>},
+ ?assertMatch({ok, [], <<"Hello World!">>},
elli_test:call('GET', <<"/hello/world/">>, [], <<>>,
?EXAMPLE_CONF)),
- ?assertEqual({ok, [], <<"Hello Test1">>},
+ ?assertMatch({ok, [], <<"Hello Test1">>},
elli_test:call('GET', <<"/hello/?name=Test1">>, [], <<>>,
?EXAMPLE_CONF)),
- ?assertEqual({ok,
+ ?assertMatch({ok,
[{<<"Content-type">>,
<<"application/json; charset=ISO-8859-1">>}],
<<"{\"name\" : \"Test2\"}">>},
diff --git a/src/elli_util.erl b/src/elli_util.erl
index e5391c1..60ff3e2 100644
--- a/src/elli_util.erl
+++ b/src/elli_util.erl
@@ -54,12 +54,16 @@ encode_range_bytes({Offset, Length}) ->
encode_range_bytes(invalid_range) -> <<"*">>.
--spec file_size(Filename::file:name()) ->
- non_neg_integer() | {error, Reason}
- when Reason :: badarg | file:posix().
+-spec file_size(Filename) -> Size | {error, Reason} when
+ Filename :: file:name_all(),
+ Size :: non_neg_integer(),
+ Reason :: file:posix() | badarg | invalid_file.
%% @doc: Get the size in bytes of the file.
file_size(Filename) ->
case file:read_file_info(Filename) of
- {ok, #file_info{size = Size}} -> Size;
- {error, Reason} -> {error, Reason}
+ {ok, #file_info{type = regular, access = Perm, size = Size}}
+ when Perm =:= read orelse Perm =:= read_write ->
+ Size;
+ {error, Reason} -> {error, Reason};
+ _ -> {error, invalid_file}
end.
diff --git a/test/elli_metrics_middleware.erl b/test/elli_metrics_middleware.erl
index dd124d9..3861c68 100644
--- a/test/elli_metrics_middleware.erl
+++ b/test/elli_metrics_middleware.erl
@@ -1,5 +1,5 @@
-module(elli_metrics_middleware).
--export([handle/2, handle_event/3]).
+-export([init/2, preprocess/2, handle/2, postprocess/3, handle_event/3]).
-behaviour(elli_handler).
@@ -7,9 +7,18 @@
%% ELLI
%%
+init(_Req, _Args) ->
+ ignore.
+
+preprocess(Req, _Args) ->
+ Req.
+
handle(_Req, _Args) ->
ignore.
+postprocess(_Req, Res, _Args) ->
+ Res.
+
%%
%% ELLI EVENT CALLBACKS
diff --git a/test/elli_ssl_tests.erl b/test/elli_ssl_tests.erl
index b6fde74..bd002c3 100644
--- a/test/elli_ssl_tests.erl
+++ b/test/elli_ssl_tests.erl
@@ -6,7 +6,8 @@ elli_ssl_test_() ->
{setup,
fun setup/0, fun teardown/1,
[
- ?_test(hello_world())
+ ?_test(hello_world()),
+ ?_test(chunked())
]}.
%%% Tests
@@ -15,6 +16,19 @@ hello_world() ->
{ok, Response} = httpc:request("https://localhost:3443/hello/world"),
?assertMatch(200, status(Response)).
+chunked() ->
+ Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1",
+
+ {ok, Response} = httpc:request("https://localhost:3443/chunked"),
+
+ ?assertMatch(200, status(Response)),
+ ?assertEqual([{"connection", "Keep-Alive"},
+ %% httpc adds a content-length, even though elli
+ %% does not send any for chunked transfers
+ {"content-length", integer_to_list(length(Expected))},
+ {"content-type", "text/event-stream"}], headers(Response)),
+ ?assertMatch(Expected, body(Response)).
+
%%% Internal helpers
setup() ->
diff --git a/test/elli_tests.erl b/test/elli_tests.erl
index b2c61af..7259ae0 100644
--- a/test/elli_tests.erl
+++ b/test/elli_tests.erl
@@ -1,13 +1,14 @@
-module(elli_tests).
-include_lib("eunit/include/eunit.hrl").
-include("elli.hrl").
+-include("elli_test.hrl").
-define(I2B(I), list_to_binary(integer_to_list(I))).
-define(I2L(I), integer_to_list(I)).
-define(README, "README.md").
-define(VTB(T1, T2, LB, UB),
- time_diff_to_micro_seconds(T1, T2) > LB andalso
- time_diff_to_micro_seconds(T1, T2) < UB).
+ time_diff_to_micro_seconds(T1, T2) >= LB andalso
+ time_diff_to_micro_seconds(T1, T2) =< UB).
time_diff_to_micro_seconds(T1, T2) ->
erlang:convert_time_unit(
@@ -22,11 +23,15 @@ elli_test_() ->
[{foreach,
fun init_stats/0, fun clear_stats/1,
[?_test(hello_world()),
+ ?_test(keep_alive_timings()),
?_test(not_found()),
?_test(crash()),
?_test(invalid_return()),
?_test(no_compress()),
+ ?_test(gzip()),
+ ?_test(deflate()),
?_test(exception_flow()),
+ ?_test(hello_iolist()),
?_test(accept_content_type()),
?_test(user_connection()),
?_test(get_args()),
@@ -34,12 +39,15 @@ elli_test_() ->
?_test(decoded_get_args_list()),
?_test(post_args()),
?_test(shorthand()),
+ ?_test(ip()),
+ ?_test(found()),
?_test(too_many_headers()),
?_test(too_big_body()),
?_test(way_too_big_body()),
?_test(bad_request_line()),
?_test(content_length()),
?_test(user_content_length()),
+ ?_test(headers()),
?_test(chunked()),
?_test(sendfile()),
?_test(send_no_file()),
@@ -65,11 +73,13 @@ setup() ->
application:start(crypto),
application:start(public_key),
application:start(ssl),
+ hackney:start(),
inets:start(),
Config = [
{mods, [
{elli_metrics_middleware, []},
+ {elli_middleware_compress, []},
{elli_example_callback, []}
]}
],
@@ -87,6 +97,43 @@ init_stats() ->
clear_stats(_) ->
ets:delete(elli_stat_table).
+
+
+accessors_test_() ->
+ RawPath = <<"/foo/bar">>,
+ Headers = [{<<"Content-Type">>, <<"application/x-www-form-urlencoded">>}],
+ Method = 'POST',
+ Body = <<"name=knut%3D">>,
+ Name = <<"knut=">>,
+ Req1 = #req{raw_path = RawPath,
+ headers = Headers,
+ method = Method,
+ body = Body},
+ Args = [{<<"name">>, Name}],
+ Req2 = #req{headers = Headers, args = Args, body = <<>>},
+
+ [
+ %% POST /foo/bar
+ ?_assertMatch(RawPath, elli_request:raw_path(Req1)),
+ ?_assertMatch(Headers, elli_request:headers(Req1)),
+ ?_assertMatch(Method, elli_request:method(Req1)),
+ ?_assertMatch(Body, elli_request:body(Req1)),
+ ?_assertMatch(Args, elli_request:post_args_decoded(Req1)),
+ ?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)),
+ ?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)),
+ ?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)),
+ %% GET /foo/bar
+ ?_assertMatch(Headers, elli_request:headers(Req2)),
+
+ ?_assertMatch(Args, elli_request:get_args(Req2)),
+ ?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)),
+ ?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)),
+ ?_assertMatch([], elli_request:post_args(Req2)),
+
+ ?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{}))
+ ].
+
+
%%% Integration tests
%%% Use inets httpc to actually call Elli over the network.
@@ -122,6 +169,65 @@ hello_world() ->
?assertMatch(true,
?VTB(send_start, send_end, 1, 200)).
+
+keep_alive_timings() ->
+
+ Transport = hackney_tcp,
+ Host = <<"localhost">>,
+ Port = 3001,
+ Options = [],
+ {ok, ConnRef} = hackney:connect(Transport, Host, Port, Options),
+
+ ReqBody = <<>>,
+ ReqHeaders = [],
+ ReqPath = <<"/hello/world">>,
+ ReqMethod = get,
+ Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody},
+
+ {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req),
+ keep_alive_timings(Status, Headers, HCRef),
+
+ %% pause between keep-alive requests,
+ %% request_start is a timestamp of
+ %% the first bytes of the second request
+ timer:sleep(1000),
+
+ {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req),
+ keep_alive_timings(Status, Headers, HCRef),
+
+ hackney:close(ConnRef).
+
+keep_alive_timings(Status, Headers, HCRef) ->
+ ?assertMatch(200, Status),
+ ?assertMatch([{<<"Connection">>,<<"Keep-Alive">>},
+ {<<"Content-Length">>,<<"12">>}], Headers),
+ ?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)),
+ %% sizes
+ ?assertMatch(63, get_size_value(resp_headers)),
+ ?assertMatch(12, get_size_value(resp_body)),
+ %% timings
+ ?assertNotMatch(undefined, get_timing_value(request_start)),
+ ?assertNotMatch(undefined, get_timing_value(headers_start)),
+ ?assertNotMatch(undefined, get_timing_value(headers_end)),
+ ?assertNotMatch(undefined, get_timing_value(body_start)),
+ ?assertNotMatch(undefined, get_timing_value(body_end)),
+ ?assertNotMatch(undefined, get_timing_value(user_start)),
+ ?assertNotMatch(undefined, get_timing_value(user_end)),
+ ?assertNotMatch(undefined, get_timing_value(send_start)),
+ ?assertNotMatch(undefined, get_timing_value(send_end)),
+ ?assertNotMatch(undefined, get_timing_value(request_end)),
+ %% check timings
+ ?assertMatch(true,
+ ?VTB(request_start, request_end, 1000000, 1200000)),
+ ?assertMatch(true,
+ ?VTB(headers_start, headers_end, 1, 100)),
+ ?assertMatch(true,
+ ?VTB(body_start, body_end, 1, 100)),
+ ?assertMatch(true,
+ ?VTB(user_start, user_end, 1000000, 1200000)),
+ ?assertMatch(true,
+ ?VTB(send_start, send_end, 1, 200)).
+
not_found() ->
{ok, Response} = httpc:request("http://localhost:3001/foobarbaz"),
?assertMatch(404, status(Response)),
@@ -145,15 +251,31 @@ invalid_return() ->
?assertMatch("Internal server error", body(Response)).
no_compress() ->
- {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed",
- [{"Accept-Encoding", "gzip"}]},
- [], []),
+ {ok, Response} = httpc:request("http://localhost:3001/compressed"),
?assertMatch(200, status(Response)),
?assertMatch([{"connection", "Keep-Alive"},
{"content-length", "1032"}], headers(Response)),
?assertEqual(binary:copy(<<"Hello World!">>, 86),
list_to_binary(body(Response))).
+compress(Encoding, Length) ->
+ {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed",
+ [{"Accept-Encoding", Encoding}]},
+ [], []),
+ ?assertMatch(200, status(Response)),
+ ?assertMatch([{"connection", "Keep-Alive"},
+ {"content-encoding", Encoding},
+ {"content-length", Length}], headers(Response)),
+ ?assertEqual(binary:copy(<<"Hello World!">>, 86),
+ uncompress(Encoding, body(Response))).
+
+uncompress("gzip", Data) -> zlib:gunzip(Data);
+uncompress("deflate", Data) -> zlib:uncompress(Data).
+
+gzip() -> compress("gzip", "41").
+
+deflate() -> compress("deflate", "29").
+
exception_flow() ->
{ok, Response} = httpc:request("http://localhost:3001/403"),
?assertMatch(403, status(Response)),
@@ -161,6 +283,11 @@ exception_flow() ->
{"content-length", "9"}], headers(Response)),
?assertMatch("Forbidden", body(Response)).
+hello_iolist() ->
+ Url = "http://localhost:3001/hello/iolist?name=knut",
+ {ok, Response} = httpc:request(Url),
+ ?assertMatch("Hello knut", body(Response)).
+
accept_content_type() ->
{ok, Json} = httpc:request(get, {"http://localhost:3001/type?name=knut",
[{"Accept", "application/json"}]}, [], []),
@@ -210,6 +337,20 @@ shorthand() ->
{"content-length", "5"}], headers(Response)),
?assertMatch("hello", body(Response)).
+ip() ->
+ {ok, Response} = httpc:request("http://localhost:3001/ip"),
+ ?assertMatch(200, status(Response)),
+ ?assertMatch("127.0.0.1", body(Response)).
+
+found() ->
+ {ok, Response} = httpc:request(get, {"http://localhost:3001/302", []},
+ [{autoredirect, false}], []),
+ ?assertMatch(302, status(Response)),
+ ?assertMatch([{"connection","Keep-Alive"},
+ {"content-length","0"},
+ {"location", "/hello/world"}], headers(Response)),
+ ?assertMatch("", body(Response)).
+
too_many_headers() ->
Headers = lists:duplicate(100, {"X-Foo", "Bar"}),
{ok, Response} = httpc:request(get, {"http://localhost:3001/foo", Headers},
@@ -262,6 +403,12 @@ user_content_length() ->
"foobar">>},
gen_tcp:recv(Client, 0)).
+headers() ->
+ {ok, Response} = httpc:request("http://localhost:3001/headers.html"),
+ Headers = headers(Response),
+
+ ?assert(proplists:is_defined("x-custom", Headers)),
+ ?assertMatch("foobar", proplists:get_value("x-custom", Headers)).
chunked() ->
Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1",
@@ -579,6 +726,10 @@ normalize_range_test_() ->
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))].
+encode_range_test() ->
+ Expected = [<<"bytes ">>,<<"*">>,<<"/">>,"42"],
+ ?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)).
+
register_test() ->
?assertMatch(undefined, whereis(elli)),
Config = [
@@ -598,14 +749,3 @@ invalid_callback_test() ->
E ->
?assertMatch(invalid_callback, E)
end.
-
-
-%%% Helpers
-
-status({{_, Status, _}, _, _}) ->
- Status.
-body({_, _, Body}) ->
- Body.
-
-headers({_, Headers, _}) ->
- lists:sort(Headers).