aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/cowboy_http.erl38
-rw-r--r--test/http_SUITE.erl50
2 files changed, 82 insertions, 6 deletions
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 7e20615..f2defdc 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -963,8 +963,17 @@ te_chunked(Data, {0, Streamed}) ->
%% @todo We are expecting an hex size, not a general token.
token(Data,
fun (<< "\r\n", Rest/binary >>, BinLen) ->
- Len = list_to_integer(binary_to_list(BinLen), 16),
- te_chunked(Rest, {Len, Streamed});
+ case list_to_integer(binary_to_list(BinLen), 16) of
+ %% Final chunk is parsed in one go above. Rest would be
+ %% <<\r\n">> if complete.
+ 0 when byte_size(Rest) < 2 ->
+ more;
+ %% Normal chunk. Add 2 to Len for trailing <<"\r\n">>. Note
+ %% that repeated <<"-2\r\n">> would be streamed, and
+ %% accumulated, until out of memory if Len could be -2.
+ Len when Len > 0 ->
+ te_chunked(Rest, {Len + 2, Streamed})
+ end;
%% Chunk size shouldn't take too many bytes,
%% don't try to stream forever.
(Rest, _) when byte_size(Rest) < 16 ->
@@ -972,11 +981,28 @@ te_chunked(Data, {0, Streamed}) ->
(_, _) ->
{error, badarg}
end);
-te_chunked(Data, {ChunkRem, Streamed}) when byte_size(Data) >= ChunkRem + 2 ->
- << Chunk:ChunkRem/binary, "\r\n", Rest/binary >> = Data,
- {ok, Chunk, Rest, {0, Streamed + byte_size(Chunk)}};
+%% <<"\n">> from trailing <<"\r\n">>.
+te_chunked(<< "\n", Rest/binary>>, {1, Streamed}) ->
+ {ok, <<>>, Rest, {0, Streamed}};
+te_chunked(<<>>, State={1, _Streamed}) ->
+ {more, 1, <<>>, State};
+%% Remainder of chunk (if any) and as much of trailing <<"\r\n">> as possible.
+te_chunked(Data, {ChunkRem, Streamed}) when byte_size(Data) >= ChunkRem - 2 ->
+ ChunkSize = ChunkRem - 2,
+ Streamed2 = Streamed + ChunkSize,
+ case Data of
+ << Chunk:ChunkSize/binary, "\r\n", Rest/binary >> ->
+ {ok, Chunk, Rest, {0, Streamed2}};
+ << Chunk:ChunkSize/binary, "\r" >> ->
+ {more, 1, Chunk, {1, Streamed2}};
+ << Chunk:ChunkSize/binary >> ->
+ {more, 2, Chunk, {2, Streamed2}}
+ end;
+%% Incomplete chunk.
te_chunked(Data, {ChunkRem, Streamed}) ->
- {more, ChunkRem + 2, Data, {ChunkRem, Streamed}}.
+ ChunkRem2 = ChunkRem - byte_size(Data),
+ Streamed2 = Streamed + byte_size(Data),
+ {more, ChunkRem2, Data, {ChunkRem2, Streamed2}}.
%% @doc Decode an identity stream.
-spec te_identity(Bin, TransferState)
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 7483599..2135fdc 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -88,6 +88,8 @@
-export([te_chunked/1]).
-export([te_chunked_chopped/1]).
-export([te_chunked_delayed/1]).
+-export([te_chunked_split_body/1]).
+-export([te_chunked_split_crlf/1]).
-export([te_identity/1]).
%% ct.
@@ -162,6 +164,8 @@ groups() ->
te_chunked,
te_chunked_chopped,
te_chunked_delayed,
+ te_chunked_split_body,
+ te_chunked_split_crlf,
te_identity
],
[
@@ -1281,6 +1285,52 @@ te_chunked_delayed(Config) ->
{ok, 200, _, Client3} = cowboy_client:response(Client2),
{ok, Body, _} = cowboy_client:response_body(Client3).
+te_chunked_split_body(Config) ->
+ Client = ?config(client, Config),
+ Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
+ Chunks = body_to_chunks(50, Body, []),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/echo/body", Config),
+ [{<<"transfer-encoding">>, <<"chunked">>}], Client),
+ {ok, Transport, Socket} = cowboy_client:transport(Client2),
+ _ = [begin
+ case Chunk of
+ %% Final chunk.
+ <<"0\r\n\r\n">> ->
+ ok = Transport:send(Socket, Chunk);
+ _ ->
+ %% Chunk of form <<"9\r\nChunkBody\r\n">>.
+ [Size, ChunkBody, <<>>] =
+ binary:split(Chunk, [<<"\r\n">>], [global]),
+ PartASize = random:uniform(byte_size(ChunkBody)),
+ <<PartA:PartASize/binary, PartB/binary>> = ChunkBody,
+ ok = Transport:send(Socket, [Size, <<"\r\n">>, PartA]),
+ ok = timer:sleep(10),
+ ok = Transport:send(Socket, [PartB, <<"\r\n">>])
+ end
+ end || Chunk <- Chunks],
+ {ok, 200, _, Client3} = cowboy_client:response(Client2),
+ {ok, Body, _} = cowboy_client:response_body(Client3).
+
+te_chunked_split_crlf(Config) ->
+ Client = ?config(client, Config),
+ Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
+ Chunks = body_to_chunks(50, Body, []),
+ {ok, Client2} = cowboy_client:request(<<"GET">>,
+ build_url("/echo/body", Config),
+ [{<<"transfer-encoding">>, <<"chunked">>}], Client),
+ {ok, Transport, Socket} = cowboy_client:transport(Client2),
+ _ = [begin
+ %% <<"\r\n">> is last 2 bytes of Chunk split before or after <<"\r">>.
+ Len = byte_size(Chunk) - (random:uniform(2) - 1),
+ <<Chunk2:Len/binary, End/binary>> = Chunk,
+ ok = Transport:send(Socket, Chunk2),
+ ok = timer:sleep(10),
+ ok = Transport:send(Socket, End)
+ end || Chunk <- Chunks],
+ {ok, 200, _, Client3} = cowboy_client:response(Client2),
+ {ok, Body, _} = cowboy_client:response_body(Client3).
+
te_identity(Config) ->
Client = ?config(client, Config),
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),