From 29043aa7b4d11e377bc76d453f592ea5a6df1f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 7 Nov 2018 18:55:06 +0100 Subject: Add support for range requests (RFC7233) in cowboy_rest This is currently undocumented but is planned to be documented in the next version. --- test/handlers/if_range_h.erl | 46 +++++++++++++++++++++ test/handlers/provide_range_callback_h.erl | 66 ++++++++++++++++++++++++++++++ test/handlers/range_satisfiable_h.erl | 39 ++++++++++++++++++ test/handlers/ranges_provided_h.erl | 30 ++++++++++++++ test/handlers/stop_handler_h.erl | 21 +++++++++- test/handlers/switch_handler_h.erl | 21 +++++++++- 6 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 test/handlers/if_range_h.erl create mode 100644 test/handlers/provide_range_callback_h.erl create mode 100644 test/handlers/range_satisfiable_h.erl create mode 100644 test/handlers/ranges_provided_h.erl (limited to 'test/handlers') diff --git a/test/handlers/if_range_h.erl b/test/handlers/if_range_h.erl new file mode 100644 index 0000000..868edbf --- /dev/null +++ b/test/handlers/if_range_h.erl @@ -0,0 +1,46 @@ +%% This module defines the ranges_provided callback +%% and a generate_etag callback that returns something +%% different depending on query string. It also defines +%% a last_modified callback that must be ignored when a +%% date is provided in if_range. + +-module(if_range_h). + +-export([init/2]). +-export([content_types_provided/2]). +-export([ranges_provided/2]). +-export([generate_etag/2]). +-export([last_modified/2]). +-export([get_text_plain/2]). +-export([get_text_plain_bytes/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +content_types_provided(Req, State) -> + {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. + +%% Simulate the callback being missing. +ranges_provided(#{qs := <<"missing-ranges_provided">>}, _) -> + no_call; +ranges_provided(Req=#{qs := <<"empty-ranges_provided">>}, State) -> + {[], Req, State}; +ranges_provided(Req, State) -> + {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. + +generate_etag(Req=#{qs := <<"weak-etag">>}, State) -> + {{weak, <<"weak-no-match">>}, Req, State}; +generate_etag(Req, State) -> + {{strong, <<"strong-and-match">>}, Req, State}. + +last_modified(Req, State) -> + {{{2222, 2, 22}, {11, 11, 11}}, Req, State}. + +get_text_plain(Req, State) -> + {<<"This is REST!">>, Req, State}. + +get_text_plain_bytes(Req, State) -> + %% We send everything in one part, since we are not testing + %% this callback specifically. + Body = <<"This is ranged REST!">>, + {[{{0, byte_size(Body) - 1, byte_size(Body)}, Body}], Req, State}. diff --git a/test/handlers/provide_range_callback_h.erl b/test/handlers/provide_range_callback_h.erl new file mode 100644 index 0000000..f14a544 --- /dev/null +++ b/test/handlers/provide_range_callback_h.erl @@ -0,0 +1,66 @@ +%% This module defines the range_satisfiable callback +%% and return something different depending on query string. + +-module(provide_range_callback_h). + +-export([init/2]). +-export([content_types_provided/2]). +-export([ranges_provided/2]). +-export([expires/2]). +-export([generate_etag/2]). +-export([last_modified/2]). +-export([get_text_plain/2]). +-export([get_text_plain_bytes/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +content_types_provided(Req, State) -> + {[ + {{<<"text">>, <<"plain">>, []}, get_text_plain}, + %% This one only exists so we generate a vary header. + {{<<"text">>, <<"html">>, []}, get_text_html} + ], Req, State}. + +ranges_provided(Req, State) -> + {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. + +generate_etag(Req=#{qs := <<"weak-etag">>}, State) -> + {{weak, <<"weak-no-match">>}, Req, State}; +generate_etag(Req, State) -> + {{strong, <<"strong-and-match">>}, Req, State}. + +last_modified(Req, State) -> + {{{2222, 2, 22}, {11, 11, 11}}, Req, State}. + +expires(Req, State) -> + {{{3333, 3, 3}, {11, 11, 11}}, Req, State}. + +get_text_plain(Req, State) -> + {<<"This is REST!">>, Req, State}. + +%% Simulate the callback being missing, otherwise expect true/false. +get_text_plain_bytes(#{qs := <<"missing">>}, _) -> + ct_helper_error_h:ignore(cowboy_rest, set_ranged_body, 3), + no_call; +get_text_plain_bytes(Req=#{range := {_, [{From=0, infinity}]}}, State) -> + %% We send everything in one part. + Body = <<"This is ranged REST!">>, + Total = byte_size(Body), + {[{{From, Total - 1, Total}, Body}], Req, State}; +get_text_plain_bytes(Req=#{range := {_, Range}}, State) -> + %% We check the range header we get and send everything hardcoded. + [ + {0, 3}, + {5, 6}, + {8, 13}, + {15, infinity} + ] = Range, + Body = <<"This is ranged REST!">>, + Total = byte_size(Body), + {[ + {{0, 3, Total}, <<"This">>}, + {{5, 6, Total}, <<"is">>}, + {{8, 13, Total}, <<"ranged">>}, + {{15, 19, Total}, <<"REST!">>} + ], Req, State}. diff --git a/test/handlers/range_satisfiable_h.erl b/test/handlers/range_satisfiable_h.erl new file mode 100644 index 0000000..357c30e --- /dev/null +++ b/test/handlers/range_satisfiable_h.erl @@ -0,0 +1,39 @@ +%% This module defines the range_satisfiable callback +%% and return something different depending on query string. + +-module(range_satisfiable_h). + +-export([init/2]). +-export([content_types_provided/2]). +-export([ranges_provided/2]). +-export([range_satisfiable/2]). +-export([get_text_plain/2]). +-export([get_text_plain_bytes/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +content_types_provided(Req, State) -> + {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. + +ranges_provided(Req, State) -> + {[{<<"bytes">>, get_text_plain_bytes}], Req, State}. + +%% Simulate the callback being missing, otherwise expect true/false. +range_satisfiable(#{qs := <<"missing">>}, _) -> + no_call; +range_satisfiable(Req=#{qs := <<"false-int">>}, State) -> + {{false, 123}, Req, State}; +range_satisfiable(Req=#{qs := <<"false-bin">>}, State) -> + {{false, <<"*/456">>}, Req, State}; +range_satisfiable(Req=#{qs := Qs}, State) -> + {Qs =:= <<"true">>, Req, State}. + +get_text_plain(Req, State) -> + {<<"This is REST!">>, Req, State}. + +get_text_plain_bytes(Req, State) -> + %% We send everything in one part, since we are not testing + %% this callback specifically. + Body = <<"This is ranged REST!">>, + {[{{0, byte_size(Body) - 1, byte_size(Body)}, Body}], Req, State}. diff --git a/test/handlers/ranges_provided_h.erl b/test/handlers/ranges_provided_h.erl new file mode 100644 index 0000000..8e2b050 --- /dev/null +++ b/test/handlers/ranges_provided_h.erl @@ -0,0 +1,30 @@ +%% This module defines the ranges_provided callback +%% and return something different depending on query string. + +-module(ranges_provided_h). + +-export([init/2]). +-export([content_types_provided/2]). +-export([ranges_provided/2]). +-export([get_text_plain/2]). + +init(Req, State) -> + {cowboy_rest, Req, State}. + +content_types_provided(Req, State) -> + {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}. + +ranges_provided(Req=#{qs := <<"list">>}, State) -> + {[ + {<<"bytes">>, get_text_plain_bytes}, + {<<"pages">>, get_text_plain_pages}, + {<<"chapters">>, get_text_plain_chapters} + ], Req, State}; +ranges_provided(Req=#{qs := <<"none">>}, State) -> + {[], Req, State}; +%% Simulate the callback being missing in other cases. +ranges_provided(_, _) -> + no_call. + +get_text_plain(Req, State) -> + {<<"This is REST!">>, Req, State}. diff --git a/test/handlers/stop_handler_h.erl b/test/handlers/stop_handler_h.erl index 22abda7..423c3f9 100644 --- a/test/handlers/stop_handler_h.erl +++ b/test/handlers/stop_handler_h.erl @@ -23,6 +23,8 @@ -export([multiple_choices/2]). -export([options/2]). -export([previously_existed/2]). +-export([range_satisfiable/2]). +-export([ranges_provided/2]). -export([rate_limited/2]). -export([resource_exists/2]). -export([service_available/2]). @@ -32,6 +34,7 @@ -export([accept/2]). -export([provide/2]). +-export([provide_range/2]). init(Req, State) -> {cowboy_rest, Req, State}. @@ -90,6 +93,12 @@ options(Req, State) -> previously_existed(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). +range_satisfiable(Req, State) -> + maybe_stop_handler(Req, State, ?FUNCTION_NAME). + +ranges_provided(Req, State) -> + maybe_stop_handler(Req, State, ?FUNCTION_NAME). + rate_limited(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). @@ -114,6 +123,9 @@ accept(Req, State) -> provide(Req, State) -> maybe_stop_handler(Req, State, ?FUNCTION_NAME). +provide_range(Req, State) -> + maybe_stop_handler(Req, State, ?FUNCTION_NAME). + maybe_stop_handler(Req=#{qs := Qs}, State, StateName) -> case atom_to_binary(StateName, latin1) of Qs -> do_stop_handler(Req, State); @@ -128,6 +140,11 @@ do_default(Req, State, content_types_accepted) -> {[{<<"text/plain">>, accept}], Req, State}; do_default(Req, State, content_types_provided) -> {[{<<"text/plain">>, provide}], Req, State}; +%% We need to accept ranges to reach these callbacks. +do_default(Req=#{qs := <<"range_satisfiable">>}, State, ranges_provided) -> + {[{<<"bytes">>, provide_range}], Req, State}; +do_default(Req=#{qs := <<"provide_range">>}, State, ranges_provided) -> + {[{<<"bytes">>, provide_range}], Req, State}; %% We need resource_exists to return false to reach these callbacks. do_default(Req=#{qs := <<"allow_missing_post">>}, State, resource_exists) -> {false, Req, State}; @@ -145,11 +162,13 @@ do_default(Req=#{qs := <<"moved_temporarily">>}, State, previously_existed) -> %% We need the DELETE to suceed to reach this callback. do_default(Req=#{qs := <<"delete_completed">>}, State, delete_resource) -> {true, Req, State}; -%% We should never reach these two callbacks. +%% We should never reach these callbacks. do_default(Req, State, accept) -> {false, Req, State}; do_default(Req, State, provide) -> {<<"This is REST!">>, Req, State}; +do_default(Req, State, provide_range) -> + {<<"This is ranged REST!">>, Req, State}; %% Simulate the callback being missing in any other cases. do_default(_, _, _) -> no_call. diff --git a/test/handlers/switch_handler_h.erl b/test/handlers/switch_handler_h.erl index cf656ae..79a84e4 100644 --- a/test/handlers/switch_handler_h.erl +++ b/test/handlers/switch_handler_h.erl @@ -22,6 +22,8 @@ -export([multiple_choices/2]). -export([options/2]). -export([previously_existed/2]). +-export([range_satisfiable/2]). +-export([ranges_provided/2]). -export([rate_limited/2]). -export([resource_exists/2]). -export([service_available/2]). @@ -31,6 +33,7 @@ -export([accept/2]). -export([provide/2]). +-export([provide_range/2]). -export([info/3]). @@ -91,6 +94,12 @@ options(Req, State) -> previously_existed(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). +range_satisfiable(Req, State) -> + maybe_switch_handler(Req, State, ?FUNCTION_NAME). + +ranges_provided(Req, State) -> + maybe_switch_handler(Req, State, ?FUNCTION_NAME). + rate_limited(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). @@ -115,6 +124,9 @@ accept(Req, State) -> provide(Req, State) -> maybe_switch_handler(Req, State, ?FUNCTION_NAME). +provide_range(Req, State) -> + maybe_switch_handler(Req, State, ?FUNCTION_NAME). + maybe_switch_handler(Req=#{qs := Qs}, State, StateName) -> case atom_to_binary(StateName, latin1) of Qs -> do_switch_handler(Req, State); @@ -129,6 +141,11 @@ do_default(Req, State, content_types_accepted) -> {[{<<"text/plain">>, accept}], Req, State}; do_default(Req, State, content_types_provided) -> {[{<<"text/plain">>, provide}], Req, State}; +%% We need to accept ranges to reach these callbacks. +do_default(Req=#{qs := <<"range_satisfiable">>}, State, ranges_provided) -> + {[{<<"bytes">>, provide_range}], Req, State}; +do_default(Req=#{qs := <<"provide_range">>}, State, ranges_provided) -> + {[{<<"bytes">>, provide_range}], Req, State}; %% We need resource_exists to return false to reach these callbacks. do_default(Req=#{qs := <<"allow_missing_post">>}, State, resource_exists) -> {false, Req, State}; @@ -146,11 +163,13 @@ do_default(Req=#{qs := <<"moved_temporarily">>}, State, previously_existed) -> %% We need the DELETE to suceed to reach this callback. do_default(Req=#{qs := <<"delete_completed">>}, State, delete_resource) -> {true, Req, State}; -%% We should never reach these two callbacks. +%% We should never reach these callbacks. do_default(Req, State, accept) -> {false, Req, State}; do_default(Req, State, provide) -> {<<"This is REST!">>, Req, State}; +do_default(Req, State, provide_range) -> + {<<"This is ranged REST!">>, Req, State}; %% Simulate the callback being missing in any other cases. do_default(_, _, _) -> no_call. -- cgit v1.2.3