aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/cowboy_rest.erl8
-rw-r--r--test/handlers/provide_range_callback_h.erl20
-rw-r--r--test/rest_handler_SUITE.erl64
3 files changed, 91 insertions, 1 deletions
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index d908b40..b874692 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1363,7 +1363,13 @@ set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) ->
%% this for non-bytes units they can always return a single range with a binary
%% content-range information.
{Ranges, Req2, State2} when length(Ranges) > 1 ->
- set_multipart_ranged_body(Req2, State2, Ranges)
+ %% We have to check whether there are sendfile tuples in the
+ %% ranges to be sent. If there are we must use stream_reply.
+ HasSendfile = [] =/= [true || {_, {sendfile, _, _, _}} <- Ranges],
+ case HasSendfile of
+ true -> send_multipart_ranged_body(Req2, State2, Ranges);
+ false -> set_multipart_ranged_body(Req2, State2, Ranges)
+ end
end catch Class:{case_clause, no_call} ->
error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}},
'A callback specified in ranges_provided/2 is not exported.'})
diff --git a/test/handlers/provide_range_callback_h.erl b/test/handlers/provide_range_callback_h.erl
index 136e37e..5b23e07 100644
--- a/test/handlers/provide_range_callback_h.erl
+++ b/test/handlers/provide_range_callback_h.erl
@@ -43,11 +43,31 @@ get_text_plain(Req, State) ->
get_text_plain_bytes(#{qs := <<"missing">>}, _) ->
ct_helper_error_h:ignore(cowboy_rest, set_ranged_body_callback, 3),
no_call;
+get_text_plain_bytes(Req=#{qs := <<"sendfile">>, range := {_, [{From=0, infinity}]}}, State) ->
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ {[{{From, Size - 1, Size}, {sendfile, From, Size, Path}}], Req, State};
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=#{qs := <<"sendfile">>, range := {_, Range}}, State) ->
+ %% We check the range header we get and send everything hardcoded.
+ [
+ {50, 99},
+ {150, 199},
+ {250, 299},
+ -99
+ ] = Range,
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ {[
+ {{50, 99, Size}, {sendfile, 50, 50, Path}},
+ {{150, 199, Size}, {sendfile, 150, 50, Path}},
+ {{250, 299, Size}, {sendfile, 250, 50, Path}},
+ {{Size - 99, Size - 1, Size}, {sendfile, Size - 99, 99, Path}}
+ ], Req, State};
get_text_plain_bytes(Req=#{range := {_, Range}}, State) ->
%% We check the range header we get and send everything hardcoded.
[
diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl
index 58898b2..363f789 100644
--- a/test/rest_handler_SUITE.erl
+++ b/test/rest_handler_SUITE.erl
@@ -413,6 +413,30 @@ provide_range_callback(Config) ->
{ok, <<"This is ranged REST!">>} = gun:await_body(ConnPid, Ref),
ok.
+provide_range_callback_sendfile(Config) ->
+ doc("A successful request for a single range results in a "
+ "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [
+ {<<"accept-encoding">>, <<"gzip">>},
+ {<<"range">>, <<"bytes=0-">>}
+ ]),
+ Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
+ Size = filelib:file_size(Path),
+ {ok, Body} = 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 0-">>,
+ 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.
+
provide_range_callback_multipart(Config) ->
doc("A successful request for multiple ranges results in a "
"206 partial content response using the multipart/byteranges "
@@ -442,6 +466,46 @@ provide_range_callback_multipart(Config) ->
<<"ThisisrangedREST!">> = BodyAcc,
ok.
+provide_range_callback_multipart_sendfile(Config) ->
+ doc("A successful request for multiple ranges results in a "
+ "206 partial content response using the multipart/byteranges "
+ "content-type and the content-range not being set. The real "
+ "content-type and content-range of the parts can be found in "
+ "the multipart headers. (RFC7233 4.1, RFC7233 A)"),
+ ConnPid = gun_open(Config),
+ Ref = gun:get(ConnPid, "/provide_range_callback?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.
+
do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) ->
case cow_multipart:parse_headers(Rest, Boundary) of
{ok, Headers, Rest1} ->