Browse Source

Add support for chunked transfer-encoding trailers

It considers all 0-sized chunks that aren't \r\n\r\n
to be trailers. There's no option for enabling/disabling
the behavior (for example when the te header was sent).
It doesn't parse the trailer, it's up to the user to
parse it separately via the new cow_http:headers/1 functions.

Note that this reuses the TotalLength part of the returned
'done' tuple to signal whether there are trailers. This value
has been ignored in Cowboy since 2.0 and was just a historical
leftover. I'm not aware of anyone using this module outside of
Gun or Cowboy, so I don't expect this to break anything. If it
does, well, it's not a documented function anyway. Your fault.
Loïc Hoguin 7 years ago
parent
commit
7728c62402
2 changed files with 33 additions and 9 deletions
  1. 6 2
      src/cow_http.erl
  2. 27 7
      src/cow_http_te.erl

+ 6 - 2
src/cow_http.erl

@@ -23,6 +23,7 @@
 
 -export([request/4]).
 -export([response/3]).
+-export([headers/1]).
 -export([version/1]).
 
 -type version() :: 'HTTP/1.0' | 'HTTP/1.1'.
@@ -254,8 +255,11 @@ request(Method, Path, Version, Headers) ->
 -spec response(status() | binary(), version(), headers()) -> iodata().
 response(Status, Version, Headers) ->
 	[version(Version), <<" ">>, status(Status), <<"\r\n">>,
-		[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers],
-		<<"\r\n">>].
+		headers(Headers), <<"\r\n">>].
+
+-spec headers(headers()) -> iodata().
+headers(Headers) ->
+	[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers].
 
 %% @doc Return the version as a binary.
 

+ 27 - 7
src/cow_http_te.erl

@@ -199,8 +199,15 @@ chunked_len(<< $f, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 15);
 %% chunk extensions (unlikely) we will need to change this clause too.
 chunked_len(<< C, R/bits >>, S, A, Len) when C =/= $\r -> skip_chunk_ext(R, S, A, Len);
 %% Final chunk.
-chunked_len(<< "\r\n\r\n", R/bits >>, S, <<>>, 0) -> {done, S, R};
-chunked_len(<< "\r\n\r\n", R/bits >>, S, A, 0) -> {done, A, S, R};
+%%
+%% When trailers are following we simply return them as the Rest.
+%% Then the user code can decide to call the stream_trailers function
+%% to parse them. The user can therefore ignore trailers as necessary
+%% if they do not wish to handle them.
+chunked_len(<< "\r\n\r\n", R/bits >>, _, <<>>, 0) -> {done, no_trailers, R};
+chunked_len(<< "\r\n\r\n", R/bits >>, _, A, 0) -> {done, A, no_trailers, R};
+chunked_len(<< "\r\n", R/bits >>, _, <<>>, 0) when byte_size(R) > 2 -> {done, trailers, R};
+chunked_len(<< "\r\n", R/bits >>, _, A, 0) when byte_size(R) > 2 -> {done, A, trailers, R};
 chunked_len(_, _, _, 0) -> more;
 %% Normal chunk. Add 2 to Len for the trailing \r\n.
 chunked_len(<< "\r\n", R/bits >>, S, A, Len) -> {next, R, {Len + 2, S}, A};
@@ -229,7 +236,7 @@ last_chunk() ->
 
 -ifdef(TEST).
 stream_chunked_identity_test() ->
-	{done, <<"Wikipedia in\r\n\r\nchunks.">>, 23, <<>>}
+	{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
 		= stream_chunked(iolist_to_binary([
 			chunk("Wiki"),
 			chunk("pedia"),
@@ -239,8 +246,8 @@ stream_chunked_identity_test() ->
 	ok.
 
 stream_chunked_one_pass_test() ->
-	{done, 0, <<>>} = stream_chunked(<<"0\r\n\r\n">>, {0, 0}),
-	{done, <<"Wikipedia in\r\n\r\nchunks.">>, 23, <<>>}
+	{done, no_trailers, <<>>} = stream_chunked(<<"0\r\n\r\n">>, {0, 0}),
+	{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
 		= stream_chunked(<<
 			"4\r\n"
 			"Wiki\r\n"
@@ -251,7 +258,7 @@ stream_chunked_one_pass_test() ->
 			"0\r\n"
 			"\r\n">>, {0, 0}),
 	%% Same but with extra spaces or chunk extensions.
-	{done, <<"Wikipedia in\r\n\r\nchunks.">>, 23, <<>>}
+	{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
 		= stream_chunked(<<
 			"4 \r\n"
 			"Wiki\r\n"
@@ -261,6 +268,19 @@ stream_chunked_one_pass_test() ->
 			" in\r\n\r\nchunks.\r\n"
 			"0;ext\r\n"
 			"\r\n">>, {0, 0}),
+	%% Same but with trailers.
+	{done, <<"Wikipedia in\r\n\r\nchunks.">>, trailers, Rest}
+		= stream_chunked(<<
+			"4\r\n"
+			"Wiki\r\n"
+			"5\r\n"
+			"pedia\r\n"
+			"e\r\n"
+			" in\r\n\r\nchunks.\r\n"
+			"0\r\n"
+			"x-foo-bar: bar foo\r\n"
+			"\r\n">>, {0, 0}),
+	{[{<<"x-foo-bar">>, <<"bar foo">>}], <<>>} = cow_http:parse_headers(Rest),
 	ok.
 
 stream_chunked_n_passes_test() ->
@@ -270,7 +290,7 @@ stream_chunked_n_passes_test() ->
 	{more, <<"Wiki">>, 0, S2} = stream_chunked(<<"Wiki\r\n">>, S1),
 	{more, <<"pedia">>, <<"e\r">>, S3} = stream_chunked(<<"5\r\npedia\r\ne\r">>, S2),
 	{more, <<" in\r\n\r\nchunks.">>, 2, S4} = stream_chunked(<<"e\r\n in\r\n\r\nchunks.">>, S3),
-	{done, 23, <<>>} = stream_chunked(<<"\r\n0\r\n\r\n">>, S4),
+	{done, no_trailers, <<>>} = stream_chunked(<<"\r\n0\r\n\r\n">>, S4),
 	%% A few extra for coverage purposes.
 	more = stream_chunked(<<"\n3">>, {1, 0}),
 	{more, <<"abc">>, 2, {2, 3}} = stream_chunked(<<"\n3\r\nabc">>, {1, 0}),