From c2e946708e479d647a81afc8d8f59992f92c6a95 Mon Sep 17 00:00:00 2001 From: Adrian Roe Date: Wed, 26 Jun 2013 18:04:20 +0100 Subject: 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. --- src/cowboy_req.erl | 53 +++++++++++++++++++++------------- test/http_SUITE.erl | 14 +++++++++ test/http_SUITE_data/http_streamed.erl | 20 +++++++++++++ 3 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 test/http_SUITE_data/http_streamed.erl diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 452d390..539d961 100644 --- a/src/cowboy_req.erl +++ b/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; diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 28849fc..f0196ec 100644 --- a/test/http_SUITE.erl +++ b/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)])), diff --git a/test/http_SUITE_data/http_streamed.erl b/test/http_SUITE_data/http_streamed.erl new file mode 100644 index 0000000..674cc40 --- /dev/null +++ b/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. -- cgit v1.2.3