diff options
-rw-r--r-- | src/cowboy_rest.erl | 177 | ||||
-rw-r--r-- | test/handlers/provide_range_callback_h.erl | 4 | ||||
-rw-r--r-- | test/handlers/ranges_provided_auto_h.erl | 27 | ||||
-rw-r--r-- | test/rest_handler_SUITE.erl | 173 |
4 files changed, 357 insertions, 24 deletions
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 8a574cd..d908b40 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -1229,15 +1229,127 @@ range_satisfiable(Req, State, Callback) -> range_not_satisfiable(Req2, State2, Iodata) end. -%% We send the content-range header when we can on error. -range_not_satisfiable(Req, State, undefined) -> - respond(Req, State, 416); -range_not_satisfiable(Req0=#{range := {RangeUnit, _}}, State, RangeData) -> - Req = cowboy_req:set_resp_header(<<"content-range">>, - [RangeUnit, $\s, RangeData], Req0), - respond(Req, State, 416). +%% When the callback selected is 'auto' and the range unit +%% is bytes, we call the normal provide callback and split +%% the content automatically. +set_ranged_body(Req=#{range := {<<"bytes">>, _}}, State, auto) -> + set_ranged_body_auto(Req, State); +set_ranged_body(Req, State, Callback) -> + set_ranged_body_callback(Req, State, Callback). + +set_ranged_body_auto(Req, State=#state{handler=Handler, content_type_a={_, Callback}}) -> + try case call(Req, State, Callback) of + {stop, Req2, State2} -> + terminate(Req2, State2); + {Switch, Req2, State2} when element(1, Switch) =:= switch_handler -> + switch_handler(Switch, Req2, State2); + {Body, Req2, State2} -> + maybe_set_ranged_body_auto(Req2, State2, Body) + end catch Class:{case_clause, no_call} -> + error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, + 'A callback specified in content_types_provided/2 is not exported.'}) + end. + +maybe_set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> + Size = case Body of + {sendfile, _, Bytes, _} -> Bytes; + _ -> iolist_size(Body) + end, + Checks = [case Range of + {From, infinity} -> From < Size; + {From, To} -> (From < Size) andalso (From =< To) andalso (To =< Size); + Neg -> (Neg =/= 0) andalso (-Neg < Size) + end || Range <- Ranges], + case lists:usort(Checks) of + [true] -> set_ranged_body_auto(Req, State, Body); + _ -> range_not_satisfiable(Req, State, [<<"*/">>, integer_to_binary(Size)]) + end. + +%% We might also want to have some checks about range order, +%% number of ranges, and perhaps also join ranges that are +%% too close into one contiguous range. Some of these can +%% be done before calling the ProvideCallback. + +set_ranged_body_auto(Req=#{range := {_, Ranges}}, State, Body) -> + Parts = [ranged_partition(Range, Body) || Range <- Ranges], + case Parts of + [OnePart] -> set_one_ranged_body(Req, State, OnePart); + _ when is_tuple(Body) -> send_multipart_ranged_body(Req, State, Parts); + _ -> set_multipart_ranged_body(Req, State, Parts) + end. + +ranged_partition(Range, {sendfile, Offset0, Bytes0, Path}) -> + {From, To, Offset, Bytes} = case Range of + {From0, infinity} -> {From0, Bytes0 - 1, Offset0 + From0, Bytes0 - From0}; + {From0, To0} -> {From0, To0, Offset0 + From0, 1 + To0 - From0}; + Neg -> {Bytes0 + Neg, Bytes0 - 1, Offset0 + Bytes0 + Neg, -Neg} + end, + {{From, To, Bytes0}, {sendfile, Offset, Bytes, Path}}; +ranged_partition(Range, Data0) -> + Total = iolist_size(Data0), + {From, To, Data} = case Range of + {From0, infinity} -> + {_, Data1} = cow_iolists:split(From0, Data0), + {From0, Total - 1, Data1}; + {From0, To0} -> + {_, Data1} = cow_iolists:split(From0, Data0), + {Data2, _} = cow_iolists:split(To0 - From0 + 1, Data1), + {From0, To0, Data2}; + Neg -> + {_, Data1} = cow_iolists:split(Total + Neg, Data0), + {Total + Neg, Total - 1, Data1} + end, + {{From, To, Total}, Data}. + +-ifdef(TEST). +ranged_partition_test_() -> + Tests = [ + %% Sendfile with open-ended range. + {{0, infinity}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {{6, infinity}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {{11, infinity}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + %% Sendfile with open-ended range. Sendfile tuple has an offset originally. + {{0, infinity}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {{6, infinity}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {{11, infinity}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + %% Sendfile with a specific range. + {{0, 11}, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {{6, 11}, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {{11, 11}, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + {{1, 10}, {sendfile, 0, 12, "t"}, {{1, 10, 12}, {sendfile, 1, 10, "t"}}}, + %% Sendfile with a specific range. Sendfile tuple has an offset originally. + {{0, 11}, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {{6, 11}, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {{11, 11}, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + {{1, 10}, {sendfile, 3, 12, "t"}, {{1, 10, 12}, {sendfile, 4, 10, "t"}}}, + %% Sendfile with negative range. + {-12, {sendfile, 0, 12, "t"}, {{0, 11, 12}, {sendfile, 0, 12, "t"}}}, + {-6, {sendfile, 0, 12, "t"}, {{6, 11, 12}, {sendfile, 6, 6, "t"}}}, + {-1, {sendfile, 0, 12, "t"}, {{11, 11, 12}, {sendfile, 11, 1, "t"}}}, + %% Sendfile with negative range. Sendfile tuple has an offset originally. + {-12, {sendfile, 3, 12, "t"}, {{0, 11, 12}, {sendfile, 3, 12, "t"}}}, + {-6, {sendfile, 3, 12, "t"}, {{6, 11, 12}, {sendfile, 9, 6, "t"}}}, + {-1, {sendfile, 3, 12, "t"}, {{11, 11, 12}, {sendfile, 14, 1, "t"}}}, + %% Iodata with open-ended range. + {{0, infinity}, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, + {{6, infinity}, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, + {{11, infinity}, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}}, + %% Iodata with a specific range. The resulting data is + %% wrapped in a list because of how cow_iolists:split/2 works. + {{0, 11}, <<"Hello world!">>, {{0, 11, 12}, [<<"Hello world!">>]}}, + {{6, 11}, <<"Hello world!">>, {{6, 11, 12}, [<<"world!">>]}}, + {{11, 11}, <<"Hello world!">>, {{11, 11, 12}, [<<"!">>]}}, + {{1, 10}, <<"Hello world!">>, {{1, 10, 12}, [<<"ello world">>]}}, + %% Iodata with negative range. + {-12, <<"Hello world!">>, {{0, 11, 12}, <<"Hello world!">>}}, + {-6, <<"Hello world!">>, {{6, 11, 12}, <<"world!">>}}, + {-1, <<"Hello world!">>, {{11, 11, 12}, <<"!">>}} + ], + [{iolist_to_binary(io_lib:format("range ~p data ~p", [VR, VD])), + fun() -> R = ranged_partition(VR, VD) end} || {VR, VD, R} <- Tests]. +-endif. -set_ranged_body(Req, State=#state{handler=Handler}, Callback) -> +set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) -> try case call(Req, State, Callback) of {stop, Req2, State2} -> terminate(Req2, State2); @@ -1245,10 +1357,7 @@ set_ranged_body(Req, State=#state{handler=Handler}, Callback) -> switch_handler(Switch, Req2, State2); %% When we receive a single range, we send it directly. {[OneRange], Req2, State2} -> - {ContentRange, Body} = prepare_range(Req2, OneRange), - Req3 = cowboy_req:set_resp_header(<<"content-range">>, ContentRange, Req2), - Req4 = cowboy_req:set_resp_body(Body, Req3), - respond(Req4, State2, 206); + set_one_ranged_body(Req2, State2, OneRange); %% When we receive multiple ranges we have to send them as multipart/byteranges. %% This also applies to non-bytes units. (RFC7233 A) If users don't want to use %% this for non-bytes units they can always return a single range with a binary @@ -1257,9 +1366,15 @@ set_ranged_body(Req, State=#state{handler=Handler}, Callback) -> set_multipart_ranged_body(Req2, State2, Ranges) end catch Class:{case_clause, no_call} -> error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, - 'A callback specified in ranges_accepted/2 is not exported.'}) + 'A callback specified in ranges_provided/2 is not exported.'}) end. +set_one_ranged_body(Req0, State, OneRange) -> + {ContentRange, Body} = prepare_range(Req0, OneRange), + Req1 = cowboy_req:set_resp_header(<<"content-range">>, ContentRange, Req0), + Req = cowboy_req:set_resp_body(Body, Req1), + respond(Req, State, 206). + set_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> Boundary = cow_multipart:boundary(), ContentType = cowboy_req:resp_header(<<"content-type">>, Req), @@ -1282,6 +1397,34 @@ set_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> Req3 = cowboy_req:set_resp_body(Body, Req2), respond(Req3, State, 206). +%% Similar to set_multipart_ranged_body except we have to stream +%% the data because the parts contain sendfile tuples. +send_multipart_ranged_body(Req, State, [FirstRange|MoreRanges]) -> + Boundary = cow_multipart:boundary(), + ContentType = cowboy_req:resp_header(<<"content-type">>, Req), + Req2 = cowboy_req:set_resp_header(<<"content-type">>, + [<<"multipart/byteranges; boundary=">>, Boundary], Req), + Req3 = cowboy_req:stream_reply(206, Req2), + {FirstContentRange, FirstPartBody} = prepare_range(Req, FirstRange), + FirstPartHead = cow_multipart:first_part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, FirstContentRange} + ]), + cowboy_req:stream_body(FirstPartHead, nofin, Req3), + cowboy_req:stream_body(FirstPartBody, nofin, Req3), + _ = [begin + {NextContentRange, NextPartBody} = prepare_range(Req, NextRange), + NextPartHead = cow_multipart:part(Boundary, [ + {<<"content-type">>, ContentType}, + {<<"content-range">>, NextContentRange} + ]), + cowboy_req:stream_body(NextPartHead, nofin, Req3), + cowboy_req:stream_body(NextPartBody, nofin, Req3), + [NextPartHead, NextPartBody] + end || NextRange <- MoreRanges], + cowboy_req:stream_body(cow_multipart:close(Boundary), fin, Req3), + terminate(Req3, State). + prepare_range(#{range := {RangeUnit, _}}, {{From, To, Total0}, Body}) -> Total = case Total0 of '*' -> <<"*">>; @@ -1293,6 +1436,14 @@ prepare_range(#{range := {RangeUnit, _}}, {{From, To, Total0}, Body}) -> prepare_range(#{range := {RangeUnit, _}}, {RangeData, Body}) -> {[RangeUnit, $\s, RangeData], Body}. +%% We send the content-range header when we can on error. +range_not_satisfiable(Req, State, undefined) -> + respond(Req, State, 416); +range_not_satisfiable(Req0=#{range := {RangeUnit, _}}, State, RangeData) -> + Req = cowboy_req:set_resp_header(<<"content-range">>, + [RangeUnit, $\s, RangeData], Req0), + respond(Req, State, 416). + %% Set the response headers and call the callback found using %% content_types_provided/2 to obtain the request body and add %% it to the response. diff --git a/test/handlers/provide_range_callback_h.erl b/test/handlers/provide_range_callback_h.erl index f14a544..136e37e 100644 --- a/test/handlers/provide_range_callback_h.erl +++ b/test/handlers/provide_range_callback_h.erl @@ -1,4 +1,4 @@ -%% This module defines the range_satisfiable callback +%% This module defines many callbacks relevant to range requests %% and return something different depending on query string. -module(provide_range_callback_h). @@ -41,7 +41,7 @@ get_text_plain(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), + ct_helper_error_h:ignore(cowboy_rest, set_ranged_body_callback, 3), no_call; get_text_plain_bytes(Req=#{range := {_, [{From=0, infinity}]}}, State) -> %% We send everything in one part. diff --git a/test/handlers/ranges_provided_auto_h.erl b/test/handlers/ranges_provided_auto_h.erl new file mode 100644 index 0000000..f7e6595 --- /dev/null +++ b/test/handlers/ranges_provided_auto_h.erl @@ -0,0 +1,27 @@ +%% This module defines the ranges_provided callback +%% which returns the auto option for bytes ranges +%% and the normal ProvideCallback that returns +%% something different depending on query string. + +-module(ranges_provided_auto_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, State) -> + {[{<<"bytes">>, auto}], Req, State}. + +get_text_plain(Req=#{qs := <<"data">>}, State) -> + {<<"This is ranged REST!">>, Req, State}; +get_text_plain(Req=#{qs := <<"sendfile">>}, State) -> + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + {{sendfile, 0, Size, Path}, Req, State}. diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl index 6116830..58898b2 100644 --- a/test/rest_handler_SUITE.erl +++ b/test/rest_handler_SUITE.erl @@ -52,6 +52,7 @@ init_dispatch(_) -> {"/provide_range_callback", provide_range_callback_h, []}, {"/range_satisfiable", range_satisfiable_h, []}, {"/ranges_provided", ranges_provided_h, []}, + {"/ranges_provided_auto", ranges_provided_auto_h, []}, {"/rate_limited", rate_limited_h, []}, {"/stop_handler", stop_handler_h, []}, {"/switch_handler", switch_handler_h, run}, @@ -431,7 +432,15 @@ provide_range_callback_multipart(Config) -> = lists:keyfind(<<"content-type">>, 1, Headers), {ok, Body0} = gun:await_body(ConnPid, Ref), Body = do_decode(Headers, Body0), - do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>). + {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), + [ + {bytes, 0, 3, 20}, + {bytes, 5, 6, 20}, + {bytes, 8, 13, 20}, + {bytes, 15, 19, 20} + ] = ContentRanges, + <<"ThisisrangedREST!">> = BodyAcc, + ok. do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) -> case cow_multipart:parse_headers(Rest, Boundary) of @@ -450,14 +459,7 @@ do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyA <<BodyAcc/binary, Body/binary>>) end; {done, <<>>} -> - [ - {bytes, 0, 3, 20}, - {bytes, 5, 6, 20}, - {bytes, 8, 13, 20}, - {bytes, 15, 19, 20} - ] = lists:reverse(ContentRangesAcc), - <<"ThisisrangedREST!">> = BodyAcc, - ok + {lists:reverse(ContentRangesAcc), BodyAcc} end. provide_range_callback_metadata(Config) -> @@ -598,6 +600,159 @@ ranges_provided_accept_ranges(Config) -> {_, <<"bytes, pages, chapters">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), ok. +%% @todo Probably should have options to do this automatically for auto at least. +%% +%% A server that supports range requests MAY ignore or reject a Range +%% header field that consists of more than two overlapping ranges, or a +%% set of many small ranges that are not listed in ascending order, +%% since both are indications of either a broken client or a deliberate +%% denial-of-service attack (Section 6.1). + +%% @todo Probably should have options for auto as well to join ranges that +%% are very close from each other. + +ranges_provided_auto_data(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"range">>, <<"bytes=8-">>} + ]), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + {_, <<"bytes 8-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers), + {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, <<"ranged REST!">>} = gun:await_body(ConnPid, Ref), + ok. + +ranges_provided_auto_sendfile(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"range">>, <<"bytes=8-">>} + ]), + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + {ok, <<_:8/binary, Body/bits>>} = file:read_file(Path), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), + ContentRange = iolist_to_binary([ + <<"bytes 8-">>, + integer_to_binary(Size - 1), + <<"/">>, + integer_to_binary(Size) + ]), + {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, Body} = gun:await_body(ConnPid, Ref), + ok. + +ranges_provided_auto_multipart_data(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ + {<<"accept-encoding">>, <<"gzip">>}, + %% This range selects everything except the space characters. + {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>} + ]), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + false = lists:keyfind(<<"content-range">>, 1, Headers), + {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} + = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, Body0} = gun:await_body(ConnPid, Ref), + Body = do_decode(Headers, Body0), + %% We will receive the ranges in the same order as requested. + {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), + [ + {bytes, 0, 3, 20}, + {bytes, 5, 6, 20}, + {bytes, 8, 13, 20}, + {bytes, 15, 19, 20} + ] = ContentRanges, + <<"ThisisrangedREST!">> = BodyAcc, + ok. + +ranges_provided_auto_multipart_sendfile(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically."), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ + {<<"accept-encoding">>, <<"gzip">>}, + %% This range selects a few random chunks of the file. + {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>} + ]), + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + Skip = Size - 399, + {ok, << + _:50/binary, Body1:50/binary, + _:50/binary, Body2:50/binary, + _:50/binary, Body3:50/binary, + _:Skip/binary, Body4/bits>>} = file:read_file(Path), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + false = lists:keyfind(<<"content-range">>, 1, Headers), + {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} + = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, Body0} = gun:await_body(ConnPid, Ref), + Body = do_decode(Headers, Body0), + %% We will receive the ranges in the same order as requested. + {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), + LastFrom = 300 + Skip, + LastTo = Size - 1, + [ + {bytes, 50, 99, Size}, + {bytes, 150, 199, Size}, + {bytes, 250, 299, Size}, + {bytes, LastFrom, LastTo, Size} + ] = ContentRanges, + BodyAcc = <<Body1/binary, Body2/binary, Body3/binary, Body4/binary>>, + ok. + +ranges_provided_auto_not_satisfiable_data(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically. When the requested " + "range is not satisfiable a 416 range not satisfiable response " + "is expected. The content-range header will be set. (RFC7233 4.4)"), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"range">>, <<"bytes=1000-">>} + ]), + {response, fin, 416, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + {_, <<"bytes */20">>} = lists:keyfind(<<"content-range">>, 1, Headers), + ok. + +ranges_provided_auto_not_satisfiable_sendfile(Config) -> + doc("When the unit range is bytes and the callback is 'auto' " + "Cowboy will call the normal ProvideCallback and perform " + "the range calculations automatically. When the requested " + "range is not satisfiable a 416 range not satisfiable response " + "is expected. The content-range header will be set. (RFC7233 4.4)"), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"range">>, <<"bytes=1000-">>} + ]), + {response, fin, 416, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + ContentRange = iolist_to_binary([<<"bytes */">>, integer_to_binary(Size)]), + {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), + ok. + ranges_provided_empty_accept_ranges_none(Config) -> doc("When the ranges_provided callback exists but returns an empty list " "the accept-ranges header is sent in the response with the value none. (RFC7233 2.3)"), |