diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | guide/rest_handlers.md | 49 | ||||
-rw-r--r-- | guide/toc.md | 5 | ||||
-rw-r--r-- | src/cowboy_bstr.erl | 4 | ||||
-rw-r--r-- | src/cowboy_clock.erl | 4 | ||||
-rw-r--r-- | src/cowboy_http.erl | 79 | ||||
-rw-r--r-- | src/cowboy_multipart.erl | 11 | ||||
-rw-r--r-- | src/cowboy_req.erl | 40 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 20 | ||||
-rw-r--r-- | src/cowboy_router.erl | 4 | ||||
-rw-r--r-- | src/cowboy_static.erl | 2 | ||||
-rw-r--r-- | test/http_SUITE.erl | 20 | ||||
-rw-r--r-- | test/rest_postonly_resource.erl | 14 |
13 files changed, 149 insertions, 105 deletions
@@ -84,7 +84,7 @@ CT_RUN = ct_run \ -logdir logs # -cover test/cover.spec -tests: ERLC_OPTS += -DTEST=1 +tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' tests: clean clean-deps deps app build-tests @mkdir -p logs/ @$(CT_RUN) -suite eunit_SUITE http_SUITE ws_SUITE diff --git a/guide/rest_handlers.md b/guide/rest_handlers.md index ac11d98..7e8427e 100644 --- a/guide/rest_handlers.md +++ b/guide/rest_handlers.md @@ -15,6 +15,21 @@ describing the resource and modifying the machine's behavior. As the REST handler is still subject to change, the documentation is still thin. This state of affair will be improved in the coming weeks. +Usage +----- + +Like Websocket, REST is a sub-protocol of HTTP. It therefore +requires a protocol upgrade. + +``` erlang +init({tcp, http}, Req, Opts) -> + {upgrade, protocol, cowboy_rest}. +``` + +Cowboy will then switch to the REST protocol and start executing +the flow diagram, starting from `rest_init/2` if it's defined, +and ending with `rest_terminate/2` also if defined. + Flow diagram ------------ @@ -70,7 +85,7 @@ empty column means there is no default value for this callback. | allow_missing_post | `true` | | charsets_provided | skip | | content_types_accepted | | -| content_types_provided | | +| content_types_provided | `[{{<<"text">>, <<"html">>, '*'}, to_html}] ` | | delete_completed | `true` | | delete_resource | `false` | | expires | `undefined` | @@ -106,6 +121,9 @@ each function. For example, `from_html` and `to_html` indicate in the first case that we're accepting a resource given as HTML, and in the second case that we send one as HTML. +Meta data +--------- + Cowboy will set informative meta values at various points of the execution. You can retrieve them using `cowboy_req:meta/{2,3}`. The values are defined in the following table. @@ -119,17 +137,18 @@ The values are defined in the following table. They can be used to reply a response entity to a request with an idempotent method (`POST`, `PUT`, `PATCH`, `DELETE`). -Usage ------ - -Like Websocket, REST is a sub-protocol of HTTP. It therefore -requires a protocol upgrade. - -``` erlang -init({tcp, http}, Req, Opts) -> - {upgrade, protocol, cowboy_rest}. -``` - -Cowboy will then switch to the REST protocol and start executing -the flow diagram, starting from `rest_init/2` if it's defined, -and ending with `rest_terminate/2` also if defined. +Response headers +---------------- + +Cowboy will set response headers automatically over the execution +of the REST code. They are listed in the following table. + +| Header name | Details | +| ---------------- | -------------------------------------------------- | +| content-language | Language used in the response body | +| content-type | Media type and charset of the response body | +| etag | Etag of the resource | +| expires | Expiration date of the resource | +| last-modified | Last modification date for the resource | +| location | Relative or absolute URI to the requested resource | +| vary | List of headers that may change the representation of the resource | diff --git a/guide/toc.md b/guide/toc.md index 44c8e22..f8eeb18 100644 --- a/guide/toc.md +++ b/guide/toc.md @@ -28,9 +28,12 @@ Cowboy User Guide * Usage * [REST handlers](rest_handlers.md) * Purpose + * Usage * Flow diagram + * Methods * Callbacks - * Usage + * Meta data + * Response headers * [Static handlers](static_handlers.md) * Purpose * Usage diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl index 01ed9ae..0c1f66a 100644 --- a/src/cowboy_bstr.erl +++ b/src/cowboy_bstr.erl @@ -24,10 +24,6 @@ -export([char_to_lower/1]). -export([char_to_upper/1]). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - %% @doc Capitalize a token. %% %% The first letter and all letters after a dash are capitalized. diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl index 71bcb21..f21616c 100644 --- a/src/cowboy_clock.erl +++ b/src/cowboy_clock.erl @@ -45,10 +45,6 @@ -define(SERVER, ?MODULE). -define(TABLE, ?MODULE). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - %% API. %% @private diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 5877ae1..f889b52 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -61,10 +61,6 @@ -export_type([headers/0]). -export_type([status/0]). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - %% Parsing. %% @doc Parse a non-empty list of the given type. @@ -1378,41 +1374,54 @@ x_www_form_urlencoded_test_() -> [{Qs, fun() -> R = x_www_form_urlencoded(Qs) end} || {Qs, R} <- Tests]. urldecode_test_() -> - U = fun urldecode/2, - [?_assertEqual(<<" ">>, U(<<"%20">>, crash)), - ?_assertEqual(<<" ">>, U(<<"+">>, crash)), - ?_assertEqual(<<0>>, U(<<"%00">>, crash)), - ?_assertEqual(<<255>>, U(<<"%fF">>, crash)), - ?_assertEqual(<<"123">>, U(<<"123">>, crash)), - ?_assertEqual(<<"%i5">>, U(<<"%i5">>, skip)), - ?_assertEqual(<<"%5">>, U(<<"%5">>, skip)), - ?_assertError(badarg, U(<<"%i5">>, crash)), - ?_assertError(badarg, U(<<"%5">>, crash)) - ]. + F = fun(Qs, O) -> + try urldecode(Qs, O) of + R -> + {ok, R} + catch _:E -> + {error, E} + end + end, + Tests = [ + {<<"%20">>, crash, {ok, <<" ">>}}, + {<<"+">>, crash, {ok, <<" ">>}}, + {<<"%00">>, crash, {ok, <<0>>}}, + {<<"%fF">>, crash, {ok, <<255>>}}, + {<<"123">>, crash, {ok, <<"123">>}}, + {<<"%i5">>, skip, {ok, <<"%i5">>}}, + {<<"%5">>, skip, {ok, <<"%5">>}}, + {<<"%i5">>, crash, {error, badarg}}, + {<<"%5">>, crash, {error, badarg}} + ], + [{Qs, fun() -> R = F(Qs,O) end} || {Qs, O, R} <- Tests]. urlencode_test_() -> - U = fun urlencode/2, - [?_assertEqual(<<"%ff%00">>, U(<<255,0>>, [])), - ?_assertEqual(<<"%FF%00">>, U(<<255,0>>, [upper])), - ?_assertEqual(<<"+">>, U(<<" ">>, [])), - ?_assertEqual(<<"%20">>, U(<<" ">>, [noplus])), - ?_assertEqual(<<"aBc">>, U(<<"aBc">>, [])), - ?_assertEqual(<<".-~_">>, U(<<".-~_">>, [])), - ?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>)) - ]. + Tests = [ + {<<255,0>>, [], <<"%ff%00">>}, + {<<255,0>>, [upper], <<"%FF%00">>}, + {<<" ">>, [], <<"+">>}, + {<<" ">>, [noplus], <<"%20">>}, + {<<"aBc">>, [], <<"aBc">>}, + {<<".-~_">>, [], <<".-~_">>} + ], + Tests2 = [{<<255, " ">>,<<"%ff+">>}], + [{V, fun() -> R = urlencode(V, O) end} || {V, O, R} <- Tests] ++ + [{V, fun() -> R = urlencode(V) end} || {V, R} <- Tests2]. http_authorization_test_() -> - [?_assertEqual({<<"basic">>, {<<"Alladin">>, <<"open sesame">>}}, - authorization(<<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, <<"basic">>)), - ?_assertEqual({error, badarg}, - authorization(<<"dXNlcm5hbWUK">>, <<"basic">>)), - ?_assertEqual({error, badarg}, - authorization(<<"_[]@#$%^&*()-AA==">>, <<"basic">>)), - ?_assertEqual({error, badarg}, - authorization(<<"dXNlcjpwYXNzCA==">>, <<"basic">>)), %% user:pass\010 - ?_assertEqual({<<"bearer">>,<<"some_secret_key">>}, - authorization(<<" some_secret_key">>, <<"bearer">>)) - ]. + Tests = [ + {<<"basic">>, <<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, + {<<"basic">>, {<<"Alladin">>, <<"open sesame">>}}}, + {<<"basic">>, <<"dXNlcm5hbWUK">>, + {error, badarg}}, + {<<"basic">>, <<"_[]@#$%^&*()-AA==">>, + {error, badarg}}, + {<<"basic">>, <<"dXNlcjpwYXNzCA==">>, + {error, badarg}}, + {<<"bearer">>, <<" some_secret_key">>, + {<<"bearer">>,<<"some_secret_key">>}} + ], + [{V, fun() -> R = authorization(V,T) end} || {T, V, R} <- Tests]. http_range_test_() -> Tests = [ diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl index 4e8fff0..4df5a27 100644 --- a/src/cowboy_multipart.erl +++ b/src/cowboy_multipart.erl @@ -30,10 +30,6 @@ -type end_of_part() :: {end_of_part, cont(more(part_result()))}. -type disposition() :: {binary(), [{binary(), binary()}]}. --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - %% API. %% @doc Return a multipart parser for the given boundary. @@ -298,8 +294,7 @@ title(Bin) -> iolist_to_binary(Title). suffix_test_() -> - [?_assertEqual(Part, suffix_match(Packet, pattern(Boundary))) || - {Part, Packet, Boundary} <- [ + Tests = [ {nomatch, <<>>, <<"ABC">>}, {{0, 1}, <<"\r">>, <<"ABC">>}, {{0, 2}, <<"\r\n">>, <<"ABC">>}, @@ -311,6 +306,8 @@ suffix_test_() -> {{1, 1}, <<"1\r">>, <<"ABC">>}, {{2, 2}, <<"12\r\n">>, <<"ABC">>}, {{3, 4}, <<"123\r\n--">>, <<"ABC">>} - ]]. + ], + [fun() -> Part = suffix_match(Packet, pattern(Boundary)) end || + {Part, Packet, Boundary} <- Tests]. -endif. diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 1817ab0..76c4085 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -115,10 +115,6 @@ -export([lock/1]). -export([to_list/1]). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - -type cookie_option() :: {max_age, non_neg_integer()} | {domain, binary()} | {path, binary()} | {secure, boolean()} | {http_only, boolean()}. @@ -1473,26 +1469,20 @@ connection_to_atom_test_() -> [{lists:flatten(io_lib:format("~p", [T])), fun() -> R = connection_to_atom(T) end} || {T, R} <- Tests]. -merge_headers_test() -> - Left0 = [{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}], - Right0 = [{<<"set-cookie">>,<<"foo=bar">>},{<<"content-length">>,<<"11">>}], - - ?assertMatch( - [{<<"set-cookie">>,<<"foo=bar">>}, - {<<"content-length">>,<<"13">>}, - {<<"server">>,<<"Cowboy">>}], - merge_headers(Left0, Right0)), - - Left1 = [{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}], - Right1 = [{<<"set-cookie">>,<<"foo=bar">>},{<<"set-cookie">>,<<"bar=baz">>}], - - ?assertMatch( - [{<<"set-cookie">>,<<"bar=baz">>}, - {<<"set-cookie">>,<<"foo=bar">>}, - {<<"content-length">>,<<"13">>}, - {<<"server">>,<<"Cowboy">>}], - merge_headers(Left1, Right1)), - - ok. +merge_headers_test_() -> + Tests = [ + {[{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}], + [{<<"set-cookie">>,<<"foo=bar">>},{<<"content-length">>,<<"11">>}], + [{<<"set-cookie">>,<<"foo=bar">>}, + {<<"content-length">>,<<"13">>}, + {<<"server">>,<<"Cowboy">>}]}, + {[{<<"content-length">>,<<"13">>},{<<"server">>,<<"Cowboy">>}], + [{<<"set-cookie">>,<<"foo=bar">>},{<<"set-cookie">>,<<"bar=baz">>}], + [{<<"set-cookie">>,<<"bar=baz">>}, + {<<"set-cookie">>,<<"foo=bar">>}, + {<<"content-length">>,<<"13">>}, + {<<"server">>,<<"Cowboy">>}]} + ], + [fun() -> Res = merge_headers(L,R) end || {L, R, Res} <- Tests]. -endif. diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index c28b627..4ba2b47 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -219,13 +219,25 @@ options(Req, State) -> content_types_provided(Req, State) -> case call(Req, State, content_types_provided) of no_call -> - not_acceptable(Req, State); + State2 = State#state{ + content_types_p=[{{<<"text">>, <<"html">>, '*'}, to_html}]}, + case cowboy_req:parse_header(<<"accept">>, Req) of + {error, badarg} -> + respond(Req, State2, 400); + {ok, undefined, Req2} -> + languages_provided( + cowboy_req:set_meta(media_type, {<<"text">>, <<"html">>, []}, Req2), + State2#state{content_type_a={{<<"text">>, <<"html">>, []}, to_html}}); + {ok, Accept, Req2} -> + Accept2 = prioritize_accept(Accept), + choose_media_type(Req2, State2, Accept2) + end; {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); {[], Req2, HandlerState} -> not_acceptable(Req2, State#state{handler_state=HandlerState}); {CTP, Req2, HandlerState} -> - CTP2 = [normalize_content_types(P) || P <- CTP], + CTP2 = [normalize_content_types(P) || P <- CTP], State2 = State#state{ handler_state=HandlerState, content_types_p=CTP2}, case cowboy_req:parse_header(<<"accept">>, Req2) of @@ -244,7 +256,7 @@ content_types_provided(Req, State) -> normalize_content_types({ContentType, Callback}) when is_binary(ContentType) -> - {cowboy_http:content_type(ContentType), Callback}; + {cowboy_http:content_type(ContentType), Callback}; normalize_content_types(Normalized) -> Normalized. @@ -779,7 +791,7 @@ accept_resource(Req, State) -> {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); {CTA, Req2, HandlerState} -> - CTA2 = [normalize_content_types(P) || P <- CTA], + CTA2 = [normalize_content_types(P) || P <- CTA], State2 = State#state{handler_state=HandlerState}, case cowboy_req:parse_header(<<"content-type">>, Req2) of {ok, ContentType, Req3} -> diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl index 91912d8..e52b70b 100644 --- a/src/cowboy_router.erl +++ b/src/cowboy_router.erl @@ -51,10 +51,6 @@ -opaque dispatch_rules() :: [dispatch_rule()]. -export_type([dispatch_rules/0]). --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. - %% @doc Compile a list of routes into the dispatch format used %% by Cowboy's routing. -spec compile(routes()) -> dispatch_rules(). diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index ae0f9e6..fd5654e 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -422,8 +422,6 @@ attr_etag_function(Args, Attrs) -> {strong, list_to_binary([H|T])}. -ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --define(_eq(E, I), ?_assertEqual(E, I)). directory_path_test_() -> PL = fun(D) -> length(filename:split(directory_path(D))) end, diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 5cc63f1..73ac127 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -64,6 +64,7 @@ -export([rest_options_default/1]). -export([rest_param_all/1]). -export([rest_patch/1]). +-export([rest_postonly/1]). -export([rest_resource_etags/1]). -export([rest_resource_etags_if_none_match/1]). -export([set_env_dispatch/1]). @@ -135,6 +136,7 @@ groups() -> rest_options_default, rest_param_all, rest_patch, + rest_postonly, rest_resource_etags, rest_resource_etags_if_none_match, set_resp_body, @@ -366,6 +368,7 @@ init_dispatch(Config) -> {"/missing_get_callbacks", rest_missing_callbacks, []}, {"/missing_put_callbacks", rest_missing_callbacks, []}, {"/nodelete", rest_nodelete_resource, []}, + {"/postonly", rest_postonly_resource, []}, {"/patch", rest_patch_resource, []}, {"/resetags", rest_resource_etags, []}, {"/rest_expires", rest_expires, []}, @@ -496,7 +499,8 @@ The document has moved _ = [{Status, Packet} = begin Ret = quick_raw(Packet, Config), {Ret, Packet} - end || {Status, Packet} <- Tests]. + end || {Status, Packet} <- Tests], + ok. check_status(Config) -> Tests = [ @@ -536,7 +540,7 @@ chunked_response(Config) -> echo_body(Config) -> Client = ?config(client, Config), {ok, [{mtu, MTU}]} = inet:ifget("lo", [mtu]), - [begin + _ = [begin Body = list_to_binary(lists:duplicate(Size, $a)), {ok, Client2} = cowboy_client:request(<<"POST">>, build_url("/echo/body", Config), @@ -544,7 +548,8 @@ echo_body(Config) -> Body, Client), {ok, 200, _, Client3} = cowboy_client:response(Client2), {ok, Body, _} = cowboy_client:response_body(Client3) - end || Size <- lists:seq(MTU - 500, MTU)]. + end || Size <- lists:seq(MTU - 500, MTU)], + ok. %% Check if sending request whose size is bigger than 1000000 bytes causes 413 echo_body_max_length(Config) -> @@ -992,6 +997,15 @@ rest_patch(Config) -> ok end || {Status, Headers, Body} <- Tests]. +rest_postonly(Config) -> + Client = ?config(client, Config), + Headers = [ + {<<"content-type">>, <<"text/plain">>} + ], + {ok, Client2} = cowboy_client:request(<<"POST">>, + build_url("/postonly", Config), Headers, "12345", Client), + {ok, 204, _, _} = cowboy_client:response(Client2). + rest_resource_get_etag(Config, Type) -> rest_resource_get_etag(Config, Type, []). diff --git a/test/rest_postonly_resource.erl b/test/rest_postonly_resource.erl new file mode 100644 index 0000000..4f725c9 --- /dev/null +++ b/test/rest_postonly_resource.erl @@ -0,0 +1,14 @@ +-module(rest_postonly_resource). +-export([init/3, allowed_methods/2, content_types_accepted/2, from_text/2]). + +init(_Transport, _Req, _Opts) -> + {upgrade, protocol, cowboy_rest}. + +allowed_methods(Req, State) -> + {[<<"POST">>], Req, State}. + +content_types_accepted(Req, State) -> + {[{{<<"text">>, <<"plain">>, '*'}, from_text}], Req, State}. + +from_text(Req, State) -> + {true, Req, State}. |