mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
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.
This commit is contained in:
parent
9d2096cd35
commit
c2e946708e
3 changed files with 67 additions and 20 deletions
|
@ -164,7 +164,8 @@
|
||||||
|
|
||||||
%% Response.
|
%% Response.
|
||||||
resp_compress = false :: boolean(),
|
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_headers = [] :: cowboy:http_headers(),
|
||||||
resp_body = <<>> :: iodata() | resp_body_fun()
|
resp_body = <<>> :: iodata() | resp_body_fun()
|
||||||
| {non_neg_integer(), resp_body_fun()}
|
| {non_neg_integer(), resp_body_fun()}
|
||||||
|
@ -946,7 +947,8 @@ reply(Status, Headers, Body, Req=#http_req{
|
||||||
socket=Socket, transport=Transport,
|
socket=Socket, transport=Transport,
|
||||||
version=Version, connection=Connection,
|
version=Version, connection=Connection,
|
||||||
method=Method, resp_compress=Compress,
|
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
|
HTTP11Headers = if
|
||||||
Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' ->
|
Transport =/= cowboy_spdy, Version =:= 'HTTP/1.1' ->
|
||||||
[{<<"connection">>, atom_to_connection(Connection)}];
|
[{<<"connection">>, atom_to_connection(Connection)}];
|
||||||
|
@ -982,10 +984,12 @@ reply(Status, Headers, Body, Req=#http_req{
|
||||||
if RespType =/= hook, Method =/= <<"HEAD">> ->
|
if RespType =/= hook, Method =/= <<"HEAD">> ->
|
||||||
ChunkFun = fun(IoData) -> chunk(IoData, Req2) end,
|
ChunkFun = fun(IoData) -> chunk(IoData, Req2) end,
|
||||||
BodyFun(ChunkFun),
|
BodyFun(ChunkFun),
|
||||||
%% Terminate the chunked body for HTTP/1.1 only.
|
%% Send the last chunk if chunked encoding was used.
|
||||||
case Version of
|
if
|
||||||
'HTTP/1.0' -> Req2;
|
Version =:= 'HTTP/1.0'; RespState =:= waiting_stream ->
|
||||||
_ -> last_chunk(Req2)
|
Req2;
|
||||||
|
true ->
|
||||||
|
last_chunk(Req2)
|
||||||
end;
|
end;
|
||||||
true -> Req2
|
true -> Req2
|
||||||
end;
|
end;
|
||||||
|
@ -1086,7 +1090,7 @@ chunk(Data, #http_req{socket=Socket, transport=cowboy_spdy,
|
||||||
resp_state=chunks}) ->
|
resp_state=chunks}) ->
|
||||||
cowboy_spdy:stream_data(Socket, Data);
|
cowboy_spdy:stream_data(Socket, Data);
|
||||||
chunk(Data, #http_req{socket=Socket, transport=Transport,
|
chunk(Data, #http_req{socket=Socket, transport=Transport,
|
||||||
resp_state=chunks, version='HTTP/1.0'}) ->
|
resp_state=stream}) ->
|
||||||
Transport:send(Socket, Data);
|
Transport:send(Socket, Data);
|
||||||
chunk(Data, #http_req{socket=Socket, transport=Transport,
|
chunk(Data, #http_req{socket=Socket, transport=Transport,
|
||||||
resp_state=chunks}) ->
|
resp_state=chunks}) ->
|
||||||
|
@ -1136,16 +1140,17 @@ ensure_response(#http_req{resp_state=done}, _) ->
|
||||||
ok;
|
ok;
|
||||||
%% No response has been sent but everything apparently went fine.
|
%% No response has been sent but everything apparently went fine.
|
||||||
%% Reply with the status code found in the second argument.
|
%% 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),
|
_ = reply(Status, [], [], Req),
|
||||||
ok;
|
ok;
|
||||||
%% Terminate the chunked body for HTTP/1.1 only.
|
%% Terminate the chunked body for HTTP/1.1 only.
|
||||||
ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) ->
|
ensure_response(#http_req{method= <<"HEAD">>}, _) ->
|
||||||
ok;
|
|
||||||
ensure_response(#http_req{version='HTTP/1.0', resp_state=chunks}, _) ->
|
|
||||||
ok;
|
ok;
|
||||||
ensure_response(Req=#http_req{resp_state=chunks}, _) ->
|
ensure_response(Req=#http_req{resp_state=chunks}, _) ->
|
||||||
_ = last_chunk(Req),
|
_ = last_chunk(Req),
|
||||||
|
ok;
|
||||||
|
ensure_response(#http_req{}, _) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% Private setter/getter API.
|
%% Private setter/getter API.
|
||||||
|
@ -1269,19 +1274,27 @@ chunked_response(Status, Headers, Req=#http_req{
|
||||||
resp_headers=[], resp_body= <<>>}};
|
resp_headers=[], resp_body= <<>>}};
|
||||||
chunked_response(Status, Headers, Req=#http_req{
|
chunked_response(Status, Headers, Req=#http_req{
|
||||||
version=Version, connection=Connection,
|
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),
|
RespConn = response_connection(Headers, Connection),
|
||||||
HTTP11Headers = case Version of
|
HTTP11Headers = if
|
||||||
'HTTP/1.1' -> [
|
Version =:= 'HTTP/1.0' -> [];
|
||||||
{<<"connection">>, atom_to_connection(Connection)},
|
true ->
|
||||||
{<<"transfer-encoding">>, <<"chunked">>}];
|
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,
|
end,
|
||||||
{RespType, Req2} = response(Status, Headers, RespHeaders, [
|
{RespType, Req2} = response(Status, Headers, RespHeaders, [
|
||||||
{<<"date">>, cowboy_clock:rfc1123()},
|
{<<"date">>, cowboy_clock:rfc1123()},
|
||||||
{<<"server">>, <<"Cowboy">>}
|
{<<"server">>, <<"Cowboy">>}
|
||||||
|HTTP11Headers], <<>>, Req),
|
|HTTP11Headers], <<>>, Req),
|
||||||
{RespType, Req2#http_req{connection=RespConn, resp_state=chunks,
|
{RespType, Req2#http_req{connection=RespConn, resp_state=RespState2,
|
||||||
resp_headers=[], resp_body= <<>>}}.
|
resp_headers=[], resp_body= <<>>}}.
|
||||||
|
|
||||||
-spec response(cowboy:http_status(), cowboy:http_headers(),
|
-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),
|
cowboy_spdy:reply(Socket, status(Status), FullHeaders, Body),
|
||||||
ReqPid ! {?MODULE, resp_sent},
|
ReqPid ! {?MODULE, resp_sent},
|
||||||
normal;
|
normal;
|
||||||
waiting ->
|
RespState when RespState =:= waiting; RespState =:= waiting_stream ->
|
||||||
HTTPVer = atom_to_binary(Version, latin1),
|
HTTPVer = atom_to_binary(Version, latin1),
|
||||||
StatusLine = << HTTPVer/binary, " ",
|
StatusLine = << HTTPVer/binary, " ",
|
||||||
(status(Status))/binary, "\r\n" >>,
|
(status(Status))/binary, "\r\n" >>,
|
||||||
|
@ -1361,7 +1374,7 @@ response_merge_headers(Headers, RespHeaders, DefaultHeaders) ->
|
||||||
merge_headers(Headers, []) ->
|
merge_headers(Headers, []) ->
|
||||||
Headers;
|
Headers;
|
||||||
merge_headers(Headers, [{<<"set-cookie">>, Value}|Tail]) ->
|
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]) ->
|
merge_headers(Headers, [{Name, Value}|Tail]) ->
|
||||||
Headers2 = case lists:keymember(Name, 1, Headers) of
|
Headers2 = case lists:keymember(Name, 1, Headers) of
|
||||||
true -> Headers;
|
true -> Headers;
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
-export([stream_body_set_resp_close/1]).
|
-export([stream_body_set_resp_close/1]).
|
||||||
-export([stream_body_set_resp_chunked/1]).
|
-export([stream_body_set_resp_chunked/1]).
|
||||||
-export([stream_body_set_resp_chunked10/1]).
|
-export([stream_body_set_resp_chunked10/1]).
|
||||||
|
-export([streamed_response/1]).
|
||||||
-export([te_chunked/1]).
|
-export([te_chunked/1]).
|
||||||
-export([te_chunked_chopped/1]).
|
-export([te_chunked_chopped/1]).
|
||||||
-export([te_chunked_delayed/1]).
|
-export([te_chunked_delayed/1]).
|
||||||
|
@ -167,6 +168,7 @@ groups() ->
|
||||||
stream_body_set_resp_close,
|
stream_body_set_resp_close,
|
||||||
stream_body_set_resp_chunked,
|
stream_body_set_resp_chunked,
|
||||||
stream_body_set_resp_chunked10,
|
stream_body_set_resp_chunked10,
|
||||||
|
streamed_response,
|
||||||
te_chunked,
|
te_chunked,
|
||||||
te_chunked_chopped,
|
te_chunked_chopped,
|
||||||
te_chunked_delayed,
|
te_chunked_delayed,
|
||||||
|
@ -352,6 +354,7 @@ init_dispatch(Config) ->
|
||||||
cowboy_router:compile([
|
cowboy_router:compile([
|
||||||
{"localhost", [
|
{"localhost", [
|
||||||
{"/chunked_response", http_chunked, []},
|
{"/chunked_response", http_chunked, []},
|
||||||
|
{"/streamed_response", http_streamed, []},
|
||||||
{"/init_shutdown", http_init_shutdown, []},
|
{"/init_shutdown", http_init_shutdown, []},
|
||||||
{"/long_polling", http_long_polling, []},
|
{"/long_polling", http_long_polling, []},
|
||||||
{"/headers/dupe", http_handler,
|
{"/headers/dupe", http_handler,
|
||||||
|
@ -1285,6 +1288,17 @@ stream_body_set_resp_chunked10(Config) ->
|
||||||
end,
|
end,
|
||||||
{error, closed} = Transport:recv(Socket, 0, 1000).
|
{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) ->
|
te_chunked(Config) ->
|
||||||
Client = ?config(client, Config),
|
Client = ?config(client, Config),
|
||||||
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
|
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
|
||||||
|
|
20
test/http_SUITE_data/http_streamed.erl
Normal file
20
test/http_SUITE_data/http_streamed.erl
Normal file
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue