Browse Source

Add a workaround to disable chunked transfer-encoding

This is an undocumented workaround to disable chunks when using HTTP/1.1.
It can be used when the client advertises itself as HTTP/1.1 despite not
understanding the chunked transfer-encoding.

Usage can be found looking at the test for it. When activated, Cowboy
will still advertise itself as HTTP/1.1, but will send the body the same
way it would if it was HTTP/1.0.
Adrian Roe 12 years ago
parent
commit
c2e946708e
3 changed files with 67 additions and 20 deletions
  1. 33 20
      src/cowboy_req.erl
  2. 14 0
      test/http_SUITE.erl
  3. 20 0
      test/http_SUITE_data/http_streamed.erl

+ 33 - 20
src/cowboy_req.erl

@@ -164,7 +164,8 @@
 
 	%% Response.
 	resp_compress = false :: boolean(),
-	resp_state = waiting :: locked | waiting | chunks | done,
+	resp_state = waiting :: locked | waiting | waiting_stream
+		| chunks | stream | done,
 	resp_headers = [] :: cowboy:http_headers(),
 	resp_body = <<>> :: iodata() | resp_body_fun()
 		| {non_neg_integer(), resp_body_fun()}
@@ -946,7 +947,8 @@ reply(Status, Headers, Body, Req=#http_req{
 		socket=Socket, transport=Transport,
 		version=Version, connection=Connection,
 		method=Method, resp_compress=Compress,
-		resp_state=waiting, resp_headers=RespHeaders}) ->
+		resp_state=RespState, resp_headers=RespHeaders})
+		when RespState =:= waiting; RespState =:= waiting_stream ->
 	HTTP11Headers = if
 		Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' ->
 			[{<<"connection">>, atom_to_connection(Connection)}];
@@ -982,10 +984,12 @@ reply(Status, Headers, Body, Req=#http_req{
 			if	RespType =/= hook, Method =/= <<"HEAD">> ->
 					ChunkFun = fun(IoData) -> chunk(IoData, Req2) end,
 					BodyFun(ChunkFun),
-					%% Terminate the chunked body for HTTP/1.1 only.
-					case Version of
-						'HTTP/1.0' -> Req2;
-						_ -> last_chunk(Req2)
+					%% Send the last chunk if chunked encoding was used.
+					if
+						Version =:= 'HTTP/1.0'; RespState =:= waiting_stream ->
+							Req2;
+						true ->
+							last_chunk(Req2)
 					end;
 				true -> Req2
 			end;
@@ -1086,7 +1090,7 @@ chunk(Data, #http_req{socket=Socket, transport=cowboy_spdy,
 		resp_state=chunks}) ->
 	cowboy_spdy:stream_data(Socket, Data);
 chunk(Data, #http_req{socket=Socket, transport=Transport,
-		resp_state=chunks, version='HTTP/1.0'}) ->
+		resp_state=stream}) ->
 	Transport:send(Socket, Data);
 chunk(Data, #http_req{socket=Socket, transport=Transport,
 		resp_state=chunks}) ->
@@ -1136,16 +1140,17 @@ ensure_response(#http_req{resp_state=done}, _) ->
 	ok;
 %% No response has been sent but everything apparently went fine.
 %% Reply with the status code found in the second argument.
-ensure_response(Req=#http_req{resp_state=waiting}, Status) ->
+ensure_response(Req=#http_req{resp_state=RespState}, Status)
+		when RespState =:= waiting; RespState =:= waiting_stream ->
 	_ = reply(Status, [], [], Req),
 	ok;
 %% Terminate the chunked body for HTTP/1.1 only.
-ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) ->
-	ok;
-ensure_response(#http_req{version='HTTP/1.0', resp_state=chunks}, _) ->
+ensure_response(#http_req{method= <<"HEAD">>}, _) ->
 	ok;
 ensure_response(Req=#http_req{resp_state=chunks}, _) ->
 	_ = last_chunk(Req),
+	ok;
+ensure_response(#http_req{}, _) ->
 	ok.
 
 %% Private setter/getter API.
@@ -1269,19 +1274,27 @@ chunked_response(Status, Headers, Req=#http_req{
 		resp_headers=[], resp_body= <<>>}};
 chunked_response(Status, Headers, Req=#http_req{
 		version=Version, connection=Connection,
-		resp_state=waiting, resp_headers=RespHeaders}) ->
+		resp_state=RespState, resp_headers=RespHeaders})
+		when RespState =:= waiting; RespState =:= waiting_stream ->
 	RespConn = response_connection(Headers, Connection),
-	HTTP11Headers = case Version of
-		'HTTP/1.1' -> [
-			{<<"connection">>, atom_to_connection(Connection)},
-			{<<"transfer-encoding">>, <<"chunked">>}];
-		_ -> []
+	HTTP11Headers = if
+		Version =:= 'HTTP/1.0' -> [];
+		true ->
+			MaybeTE = if
+				RespState =:= waiting_stream -> [];
+				true -> [{<<"transfer-encoding">>, <<"chunked">>}]
+			end,
+			[{<<"connection">>, atom_to_connection(Connection)}|MaybeTE]
+	end,
+	RespState2 = if
+		Version =:= 'HTTP/1.1', RespState =:= 'waiting' -> chunks;
+		true -> stream
 	end,
 	{RespType, Req2} = response(Status, Headers, RespHeaders, [
 		{<<"date">>, cowboy_clock:rfc1123()},
 		{<<"server">>, <<"Cowboy">>}
 	|HTTP11Headers], <<>>, Req),
-	{RespType, Req2#http_req{connection=RespConn, resp_state=chunks,
+	{RespType, Req2#http_req{connection=RespConn, resp_state=RespState2,
 			resp_headers=[], resp_body= <<>>}}.
 
 -spec response(cowboy:http_status(), cowboy:http_headers(),
@@ -1313,7 +1326,7 @@ response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{
 			cowboy_spdy:reply(Socket, status(Status), FullHeaders, Body),
 			ReqPid ! {?MODULE, resp_sent},
 			normal;
-		waiting ->
+		RespState when RespState =:= waiting; RespState =:= waiting_stream ->
 			HTTPVer = atom_to_binary(Version, latin1),
 			StatusLine = << HTTPVer/binary, " ",
 				(status(Status))/binary, "\r\n" >>,
@@ -1361,7 +1374,7 @@ response_merge_headers(Headers, RespHeaders, DefaultHeaders) ->
 merge_headers(Headers, []) ->
 	Headers;
 merge_headers(Headers, [{<<"set-cookie">>, Value}|Tail]) ->
-  merge_headers([{<<"set-cookie">>, Value}|Headers], Tail);
+	merge_headers([{<<"set-cookie">>, Value}|Headers], Tail);
 merge_headers(Headers, [{Name, Value}|Tail]) ->
 	Headers2 = case lists:keymember(Name, 1, Headers) of
 		true -> Headers;

+ 14 - 0
test/http_SUITE.erl

@@ -90,6 +90,7 @@
 -export([stream_body_set_resp_close/1]).
 -export([stream_body_set_resp_chunked/1]).
 -export([stream_body_set_resp_chunked10/1]).
+-export([streamed_response/1]).
 -export([te_chunked/1]).
 -export([te_chunked_chopped/1]).
 -export([te_chunked_delayed/1]).
@@ -167,6 +168,7 @@ groups() ->
 		stream_body_set_resp_close,
 		stream_body_set_resp_chunked,
 		stream_body_set_resp_chunked10,
+		streamed_response,
 		te_chunked,
 		te_chunked_chopped,
 		te_chunked_delayed,
@@ -352,6 +354,7 @@ init_dispatch(Config) ->
 	cowboy_router:compile([
 		{"localhost", [
 			{"/chunked_response", http_chunked, []},
+			{"/streamed_response", http_streamed, []},
 			{"/init_shutdown", http_init_shutdown, []},
 			{"/long_polling", http_long_polling, []},
 			{"/headers/dupe", http_handler,
@@ -1285,6 +1288,17 @@ stream_body_set_resp_chunked10(Config) ->
 	end,
 	{error, closed} = Transport:recv(Socket, 0, 1000).
 
+streamed_response(Config) ->
+	Client = ?config(client, Config),
+	{ok, Client2} = cowboy_client:request(<<"GET">>,
+		build_url("/streamed_response", Config), Client),
+	{ok, 200, Headers, Client3} = cowboy_client:response(Client2),
+	false = lists:keymember(<<"transfer-encoding">>, 1, Headers),
+	{ok, Transport, Socket} = cowboy_client:transport(Client3),
+	{ok, <<"streamed_handler\r\nworks fine!">>}
+		= Transport:recv(Socket, 29, 1000),
+	{error, closed} = cowboy_client:response(Client3).
+
 te_chunked(Config) ->
 	Client = ?config(client, Config),
 	Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),

+ 20 - 0
test/http_SUITE_data/http_streamed.erl

@@ -0,0 +1,20 @@
+%% Feel free to use, reuse and abuse the code in this file.
+
+-module(http_streamed).
+-behaviour(cowboy_http_handler).
+-export([init/3, handle/2, terminate/3]).
+
+init({_Transport, http}, Req, _Opts) ->
+	{ok, Req, undefined}.
+
+handle(Req, State) ->
+	Req2 = cowboy_req:set([{resp_state, waiting_stream}], Req),
+	{ok, Req3} = cowboy_req:chunked_reply(200, Req2),
+	timer:sleep(100),
+	cowboy_req:chunk("streamed_handler\r\n", Req3),
+	timer:sleep(100),
+	cowboy_req:chunk("works fine!", Req3),
+	{ok, Req3, State}.
+
+terminate(_, _, _) ->
+	ok.