From 5ebc9aa2199f95cd84d3f63383094c7ffb7edfa7 Mon Sep 17 00:00:00 2001 From: Tony Han Date: Mon, 8 Jul 2019 11:37:04 +0800 Subject: [PATCH] Add test for lingering_data handling --- Makefile | 2 +- rebar.config | 2 +- test/handlers/loop_handler_abort_h.erl | 21 ++++++++++ test/rfc7540_SUITE.erl | 54 ++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 test/handlers/loop_handler_abort_h.erl diff --git a/Makefile b/Makefile index 039bf2e3..1fc661b4 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ CT_OPTS += -ct_hooks cowboy_ct_hook [] # -boot start_sasl LOCAL_DEPS = crypto DEPS = cowlib ranch -dep_cowlib = git https://github.com/ninenines/cowlib 2.7.3 +dep_cowlib = git https://github.com/elixir-grpc/cowlib.git fix-lingering-data dep_ranch = git https://github.com/ninenines/ranch 1.7.1 DOC_DEPS = asciideck diff --git a/rebar.config b/rebar.config index bb6e0ef1..b9527b12 100644 --- a/rebar.config +++ b/rebar.config @@ -1,4 +1,4 @@ {deps, [ -{cowlib,".*",{git,"https://github.com/ninenines/cowlib","2.7.3"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.7.1"}} +{cowlib,".*",{git,"https://github.com/elixir-grpc/cowlib.git","fix-lingering-data"}},{ranch,".*",{git,"https://github.com/ninenines/ranch","1.7.1"}} ]}. {erl_opts, [debug_info,warn_export_vars,warn_shadow_vars,warn_obsolete_guard,warn_missing_spec,warn_untyped_record]}. diff --git a/test/handlers/loop_handler_abort_h.erl b/test/handlers/loop_handler_abort_h.erl new file mode 100644 index 00000000..c6018bf3 --- /dev/null +++ b/test/handlers/loop_handler_abort_h.erl @@ -0,0 +1,21 @@ +%% This module implements a loop handler that reads +%% 1000 bytes request body after sending itself a message, +%% then finish the stream. + +-module(loop_handler_abort_h). + +-export([init/2]). +-export([info/3]). +-export([terminate/3]). + +init(Req, _) -> + self() ! timeout, + {cowboy_loop, Req, undefined, hibernate}. + +info(timeout, Req0, State) -> + {_Status, Body, Req} = cowboy_req:read_body(Req0, #{length => 1000}), + 1000 = byte_size(Body), + {stop, cowboy_req:reply(200, Req), State}. + +terminate(stop, _, _) -> + ok. diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 47c213d6..ed77f58b 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -49,6 +49,7 @@ init_routes(_) -> [ {"/", hello_h, []}, {"/echo/:key", echo_h, []}, {"/long_polling", long_polling_h, []}, + {"/loop_handler_abort", loop_handler_abort_h, []}, {"/resp/:key[/:arg]", resp_h, []} ]} ]. @@ -3116,6 +3117,59 @@ data_reject_overflow_stream(Config0) -> cowboy:stop_listener(?FUNCTION_NAME) end. +lingering_data_account_for_connection_window_when(Config0) -> + doc("A receiver that receives a flow-controlled frame MUST " + "always account for its contribution against the connection " + "flow-control window"), + %% Create a new listener that allows only a single concurrent stream. + Config = cowboy_test:init_http(?FUNCTION_NAME, #{ + env => #{dispatch => cowboy_router:compile(init_routes(Config0))}, + initial_connection_window_size => 100000 + }, Config0), + try + %% We need to do the handshake manually because a WINDOW_UPDATE + %% frame will be sent to update the connection window. + {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), + %% Send a valid preface. + ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), + %% Receive the server preface. + {ok, << Len1:24 >>} = gen_tcp:recv(Socket, 3, 1000), + {ok, << 4:8, 0:40, _:Len1/binary >>} = gen_tcp:recv(Socket, 6 + Len1, 1000), + %% Send the SETTINGS ack. + ok = gen_tcp:send(Socket, cow_http2:settings_ack()), + %% Receive the WINDOW_UPDATE for the connection. + {ok, << 4:24, 8:8, 0:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), + %% Receive the SETTINGS ack. + {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), + Headers = [ + {<<":method">>, <<"POST">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"localhost">>}, %% @todo Correct port number. + {<<":path">>, <<"/loop_handler_abort">>} + ], + {HeadersBlock, _} = cow_hpack:encode(Headers), + ok = gen_tcp:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock), + cow_http2:data(1, nofin, <<0:1000/unit:8>>) + ]), + % Make sure server send RST_STREAM + timer:sleep(100), + ok = gen_tcp:send(Socket, [ + cow_http2:data(1, fin, <<0:1000/unit:8>>) + ]), + {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), + % Skip the header + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + % @todo: A WINDOW_UPDATE frame is expected to be received, but cowboy_stream_h + % doesn't send it when only first 1000 bytes of body is read + % Skip RST_STREAM + {ok, << 4:24, 3:8, 1:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), + % Received a WINDOW_UPDATE frame after got RST_STREAM + {ok, << 4:24, 8:8, 0:40, 1000:32 >>} = gen_tcp:recv(Socket, 13, 1000) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + %% (RFC7540 6.9.1) % Frames with zero length with the END_STREAM flag set (that % is, an empty DATA frame) MAY be sent if there is no available space