http_SUITE.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. %% Copyright (c) 2018, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(http_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(ct_helper, [get_remote_pid_tcp/1]).
  20. -import(cowboy_test, [gun_open/1]).
  21. -import(cowboy_test, [gun_down/1]).
  22. -import(cowboy_test, [raw_open/1]).
  23. -import(cowboy_test, [raw_send/2]).
  24. -import(cowboy_test, [raw_recv_head/1]).
  25. -import(cowboy_test, [raw_recv/3]).
  26. -import(cowboy_test, [raw_expect_recv/2]).
  27. all() -> [{group, clear}].
  28. groups() -> [{clear, [parallel], ct_helper:all(?MODULE)}].
  29. init_per_group(Name, Config) ->
  30. cowboy_test:init_http(Name, #{
  31. env => #{dispatch => init_dispatch(Config)}
  32. }, Config).
  33. end_per_group(Name, _) ->
  34. cowboy:stop_listener(Name).
  35. init_dispatch(_) ->
  36. cowboy_router:compile([{"localhost", [
  37. {"/", hello_h, []},
  38. {"/echo/:key", echo_h, []},
  39. {"/resp/:key[/:arg]", resp_h, []},
  40. {"/set_options/:key", set_options_h, []}
  41. ]}]).
  42. chunked_false(Config) ->
  43. doc("Confirm the option chunked => false disables chunked "
  44. "transfer-encoding for HTTP/1.1 connections."),
  45. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  46. env => #{dispatch => init_dispatch(Config)},
  47. chunked => false
  48. }),
  49. Port = ranch:get_port(?FUNCTION_NAME),
  50. try
  51. Request = "GET /resp/stream_reply2/200 HTTP/1.1\r\nhost: localhost\r\n\r\n",
  52. Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]),
  53. ok = raw_send(Client, Request),
  54. Rest = case catch raw_recv_head(Client) of
  55. {'EXIT', _} -> error(closed);
  56. Data ->
  57. %% Cowboy always advertises itself as HTTP/1.1.
  58. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  59. {Headers, Rest1} = cow_http:parse_headers(Rest0),
  60. false = lists:keyfind(<<"content-length">>, 1, Headers),
  61. false = lists:keyfind(<<"transfer-encoding">>, 1, Headers),
  62. Rest1
  63. end,
  64. Bits = 8000000 - bit_size(Rest),
  65. raw_expect_recv(Client, <<0:Bits>>),
  66. {error, closed} = raw_recv(Client, 1, 1000)
  67. after
  68. cowboy:stop_listener(?FUNCTION_NAME)
  69. end.
  70. chunked_one_byte_at_a_time(Config) ->
  71. doc("Confirm that chunked transfer-encoding works when "
  72. "the body is received one byte at a time."),
  73. Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
  74. ChunkedBody = iolist_to_binary(do_chunked_body(50, Body, [])),
  75. Client = raw_open(Config),
  76. ok = raw_send(Client,
  77. "POST /echo/read_body HTTP/1.1\r\n"
  78. "Host: localhost\r\n"
  79. "Transfer-encoding: chunked\r\n\r\n"),
  80. _ = [begin
  81. raw_send(Client, <<C>>),
  82. timer:sleep(10)
  83. end || <<C>> <= ChunkedBody],
  84. Rest = case catch raw_recv_head(Client) of
  85. {'EXIT', _} -> error(closed);
  86. Data ->
  87. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  88. {_, Rest1} = cow_http:parse_headers(Rest0),
  89. Rest1
  90. end,
  91. RestSize = byte_size(Rest),
  92. <<Rest:RestSize/binary, Expect/bits>> = Body,
  93. raw_expect_recv(Client, Expect).
  94. chunked_one_chunk_at_a_time(Config) ->
  95. doc("Confirm that chunked transfer-encoding works when "
  96. "the body is received one chunk at a time."),
  97. Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
  98. Chunks = do_chunked_body(50, Body, []),
  99. Client = raw_open(Config),
  100. ok = raw_send(Client,
  101. "POST /echo/read_body HTTP/1.1\r\n"
  102. "Host: localhost\r\n"
  103. "Transfer-encoding: chunked\r\n\r\n"),
  104. _ = [begin
  105. raw_send(Client, Chunk),
  106. timer:sleep(10)
  107. end || Chunk <- Chunks],
  108. Rest = case catch raw_recv_head(Client) of
  109. {'EXIT', _} -> error(closed);
  110. Data ->
  111. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  112. {_, Rest1} = cow_http:parse_headers(Rest0),
  113. Rest1
  114. end,
  115. RestSize = byte_size(Rest),
  116. <<Rest:RestSize/binary, Expect/bits>> = Body,
  117. raw_expect_recv(Client, Expect).
  118. chunked_split_delay_in_chunk_body(Config) ->
  119. doc("Confirm that chunked transfer-encoding works when "
  120. "the body is received with a delay inside the chunks."),
  121. Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
  122. Chunks = do_chunked_body(50, Body, []),
  123. Client = raw_open(Config),
  124. ok = raw_send(Client,
  125. "POST /echo/read_body HTTP/1.1\r\n"
  126. "Host: localhost\r\n"
  127. "Transfer-encoding: chunked\r\n\r\n"),
  128. _ = [begin
  129. case Chunk of
  130. <<"0\r\n\r\n">> ->
  131. raw_send(Client, Chunk);
  132. _ ->
  133. [Size, ChunkBody, <<>>] = binary:split(Chunk, <<"\r\n">>, [global]),
  134. PartASize = rand:uniform(byte_size(ChunkBody)),
  135. <<PartA:PartASize/binary, PartB/binary>> = ChunkBody,
  136. raw_send(Client, [Size, <<"\r\n">>, PartA]),
  137. timer:sleep(10),
  138. raw_send(Client, [PartB, <<"\r\n">>])
  139. end
  140. end || Chunk <- Chunks],
  141. Rest = case catch raw_recv_head(Client) of
  142. {'EXIT', _} -> error(closed);
  143. Data ->
  144. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  145. {_, Rest1} = cow_http:parse_headers(Rest0),
  146. Rest1
  147. end,
  148. RestSize = byte_size(Rest),
  149. <<Rest:RestSize/binary, Expect/bits>> = Body,
  150. raw_expect_recv(Client, Expect).
  151. chunked_split_delay_in_chunk_crlf(Config) ->
  152. doc("Confirm that chunked transfer-encoding works when "
  153. "the body is received with a delay inside the chunks end CRLF."),
  154. Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
  155. Chunks = do_chunked_body(50, Body, []),
  156. Client = raw_open(Config),
  157. ok = raw_send(Client,
  158. "POST /echo/read_body HTTP/1.1\r\n"
  159. "Host: localhost\r\n"
  160. "Transfer-encoding: chunked\r\n\r\n"),
  161. _ = [begin
  162. Len = byte_size(Chunk) - (rand:uniform(2) - 1),
  163. <<Begin:Len/binary, End/binary>> = Chunk,
  164. raw_send(Client, Begin),
  165. timer:sleep(10),
  166. raw_send(Client, End)
  167. end || Chunk <- Chunks],
  168. Rest = case catch raw_recv_head(Client) of
  169. {'EXIT', _} -> error(closed);
  170. Data ->
  171. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  172. {_, Rest1} = cow_http:parse_headers(Rest0),
  173. Rest1
  174. end,
  175. RestSize = byte_size(Rest),
  176. <<Rest:RestSize/binary, Expect/bits>> = Body,
  177. raw_expect_recv(Client, Expect).
  178. do_chunked_body(_, <<>>, Acc) ->
  179. lists:reverse([cow_http_te:last_chunk()|Acc]);
  180. do_chunked_body(ChunkSize0, Data, Acc) ->
  181. ChunkSize = min(byte_size(Data), ChunkSize0),
  182. <<Chunk:ChunkSize/binary, Rest/binary>> = Data,
  183. do_chunked_body(ChunkSize, Rest,
  184. [iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]).
  185. http10_keepalive_false(Config) ->
  186. doc("Confirm the option http10_keepalive => false disables keep-alive "
  187. "completely for HTTP/1.0 connections."),
  188. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  189. env => #{dispatch => init_dispatch(Config)},
  190. http10_keepalive => false
  191. }),
  192. Port = ranch:get_port(?FUNCTION_NAME),
  193. try
  194. Keepalive = "GET / HTTP/1.0\r\nhost: localhost\r\nConnection: keep-alive\r\n\r\n",
  195. Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]),
  196. ok = raw_send(Client, Keepalive),
  197. _ = case catch raw_recv_head(Client) of
  198. {'EXIT', _} -> error(closed);
  199. Data ->
  200. %% Cowboy always advertises itself as HTTP/1.1.
  201. {'HTTP/1.1', 200, _, Rest} = cow_http:parse_status_line(Data),
  202. {Headers, _} = cow_http:parse_headers(Rest),
  203. {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, Headers)
  204. end,
  205. ok = raw_send(Client, Keepalive),
  206. case catch raw_recv_head(Client) of
  207. {'EXIT', _} -> closed;
  208. _ -> error(not_closed)
  209. end
  210. after
  211. cowboy:stop_listener(?FUNCTION_NAME)
  212. end.
  213. idle_timeout_infinity(Config) ->
  214. doc("Ensure the idle_timeout option accepts the infinity value."),
  215. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  216. env => #{dispatch => init_dispatch(Config)},
  217. idle_timeout => infinity
  218. }),
  219. Port = ranch:get_port(?FUNCTION_NAME),
  220. try
  221. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  222. {ok, http} = gun:await_up(ConnPid),
  223. timer:sleep(500),
  224. #{socket := Socket} = gun:info(ConnPid),
  225. Pid = get_remote_pid_tcp(Socket),
  226. _ = gun:post(ConnPid, "/echo/read_body",
  227. [{<<"content-type">>, <<"text/plain">>}]),
  228. Ref = erlang:monitor(process, Pid),
  229. receive
  230. {'DOWN', Ref, process, Pid, Reason} ->
  231. error(Reason)
  232. after 1000 ->
  233. ok
  234. end
  235. after
  236. cowboy:stop_listener(?FUNCTION_NAME)
  237. end.
  238. persistent_term_router(Config) ->
  239. doc("The router can retrieve the routes from persistent_term storage."),
  240. case erlang:function_exported(persistent_term, get, 1) of
  241. true -> do_persistent_term_router(Config);
  242. false -> {skip, "This test uses the persistent_term functionality added in Erlang/OTP 21.2."}
  243. end.
  244. do_persistent_term_router(Config) ->
  245. persistent_term:put(?FUNCTION_NAME, init_dispatch(Config)),
  246. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  247. env => #{dispatch => {persistent_term, ?FUNCTION_NAME}}
  248. }),
  249. Port = ranch:get_port(?FUNCTION_NAME),
  250. try
  251. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  252. {ok, http} = gun:await_up(ConnPid),
  253. StreamRef = gun:get(ConnPid, "/"),
  254. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
  255. gun:close(ConnPid)
  256. after
  257. cowboy:stop_listener(?FUNCTION_NAME)
  258. end.
  259. request_timeout_infinity(Config) ->
  260. doc("Ensure the request_timeout option accepts the infinity value."),
  261. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  262. env => #{dispatch => init_dispatch(Config)},
  263. request_timeout => infinity
  264. }),
  265. Port = ranch:get_port(?FUNCTION_NAME),
  266. try
  267. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  268. {ok, http} = gun:await_up(ConnPid),
  269. timer:sleep(500),
  270. #{socket := Socket} = gun:info(ConnPid),
  271. Pid = get_remote_pid_tcp(Socket),
  272. Ref = erlang:monitor(process, Pid),
  273. receive
  274. {'DOWN', Ref, process, Pid, Reason} ->
  275. error(Reason)
  276. after 1000 ->
  277. ok
  278. end
  279. after
  280. cowboy:stop_listener(?FUNCTION_NAME)
  281. end.
  282. set_options_chunked_false(Config) ->
  283. doc("Confirm the option chunked can be dynamically set to disable "
  284. "chunked transfer-encoding. This results in the closing of the "
  285. "connection after the current request."),
  286. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  287. env => #{dispatch => init_dispatch(Config)},
  288. chunked => true
  289. }),
  290. Port = ranch:get_port(?FUNCTION_NAME),
  291. try
  292. Request = "GET /set_options/chunked_false HTTP/1.1\r\nhost: localhost\r\n\r\n",
  293. Client = raw_open([{type, tcp}, {port, Port}, {opts, []}|Config]),
  294. ok = raw_send(Client, Request),
  295. Rest = case catch raw_recv_head(Client) of
  296. {'EXIT', _} -> error(closed);
  297. Data ->
  298. %% Cowboy always advertises itself as HTTP/1.1.
  299. {'HTTP/1.1', 200, _, Rest0} = cow_http:parse_status_line(Data),
  300. {Headers, Rest1} = cow_http:parse_headers(Rest0),
  301. false = lists:keyfind(<<"content-length">>, 1, Headers),
  302. false = lists:keyfind(<<"transfer-encoding">>, 1, Headers),
  303. Rest1
  304. end,
  305. Bits = 8000000 - bit_size(Rest),
  306. raw_expect_recv(Client, <<0:Bits>>),
  307. {error, closed} = raw_recv(Client, 1, 1000)
  308. after
  309. cowboy:stop_listener(?FUNCTION_NAME)
  310. end.
  311. set_options_chunked_false_ignored(Config) ->
  312. doc("Confirm the option chunked can be dynamically set to disable "
  313. "chunked transfer-encoding, and that it is ignored if the "
  314. "response is not streamed."),
  315. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  316. env => #{dispatch => init_dispatch(Config)},
  317. chunked => true
  318. }),
  319. Port = ranch:get_port(?FUNCTION_NAME),
  320. try
  321. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  322. %% We do a first request setting the option but not
  323. %% using chunked transfer-encoding in the response.
  324. StreamRef1 = gun:get(ConnPid, "/set_options/chunked_false_ignored"),
  325. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
  326. {ok, <<"Hello world!">>} = gun:await_body(ConnPid, StreamRef1),
  327. %% We then do a second request to confirm that chunked
  328. %% is not disabled for that second request.
  329. StreamRef2 = gun:get(ConnPid, "/resp/stream_reply2/200"),
  330. {response, nofin, 200, Headers} = gun:await(ConnPid, StreamRef2),
  331. {_, <<"chunked">>} = lists:keyfind(<<"transfer-encoding">>, 1, Headers)
  332. after
  333. cowboy:stop_listener(?FUNCTION_NAME)
  334. end.
  335. set_options_idle_timeout(Config) ->
  336. doc("Confirm that the idle_timeout option can be dynamically "
  337. "set to change how long Cowboy will wait before it closes the connection."),
  338. %% We start with a long timeout and then cut it short.
  339. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  340. env => #{dispatch => init_dispatch(Config)},
  341. idle_timeout => 60000
  342. }),
  343. Port = ranch:get_port(?FUNCTION_NAME),
  344. try
  345. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  346. {ok, http} = gun:await_up(ConnPid),
  347. timer:sleep(500),
  348. #{socket := Socket} = gun:info(ConnPid),
  349. Pid = get_remote_pid_tcp(Socket),
  350. _ = gun:post(ConnPid, "/set_options/idle_timeout_short",
  351. [{<<"content-type">>, <<"text/plain">>}]),
  352. Ref = erlang:monitor(process, Pid),
  353. receive
  354. {'DOWN', Ref, process, Pid, _} ->
  355. ok
  356. after 2000 ->
  357. error(timeout)
  358. end
  359. after
  360. cowboy:stop_listener(?FUNCTION_NAME)
  361. end.
  362. set_options_idle_timeout_only_applies_to_current_request(Config) ->
  363. doc("Confirm that changes to the idle_timeout option only apply to the current stream."),
  364. %% We start with a long timeout and then cut it short.
  365. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
  366. env => #{dispatch => init_dispatch(Config)},
  367. idle_timeout => 500
  368. }),
  369. Port = ranch:get_port(?FUNCTION_NAME),
  370. try
  371. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  372. {ok, http} = gun:await_up(ConnPid),
  373. timer:sleep(500),
  374. #{socket := Socket} = gun:info(ConnPid),
  375. Pid = get_remote_pid_tcp(Socket),
  376. StreamRef = gun:post(ConnPid, "/set_options/idle_timeout_long",
  377. [{<<"content-type">>, <<"text/plain">>}]),
  378. Ref = erlang:monitor(process, Pid),
  379. receive
  380. {'DOWN', Ref, process, Pid, Reason} ->
  381. error(Reason)
  382. after 2000 ->
  383. ok
  384. end,
  385. %% Finish the first request and start a second one to confirm
  386. %% the idle_timeout option is back to normal.
  387. gun:data(ConnPid, StreamRef, fin, <<"Hello!">>),
  388. {response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
  389. {ok, <<"Hello!">>} = gun:await_body(ConnPid, StreamRef),
  390. _ = gun:post(ConnPid, "/echo/read_body",
  391. [{<<"content-type">>, <<"text/plain">>}]),
  392. receive
  393. {'DOWN', Ref, process, Pid, _} ->
  394. ok
  395. after 2000 ->
  396. error(timeout)
  397. end
  398. after
  399. cowboy:stop_listener(?FUNCTION_NAME)
  400. end.
  401. switch_protocol_flush(Config) ->
  402. doc("Confirm that switch_protocol does not flush unrelated messages."),
  403. ProtoOpts = #{
  404. env => #{dispatch => init_dispatch(Config)},
  405. stream_handlers => [switch_protocol_flush_h]
  406. },
  407. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
  408. Port = ranch:get_port(?FUNCTION_NAME),
  409. try
  410. Self = self(),
  411. ConnPid = gun_open([{port, Port}, {type, tcp}, {protocol, http}|Config]),
  412. _ = gun:get(ConnPid, "/", [
  413. {<<"x-test-pid">>, pid_to_list(Self)}
  414. ]),
  415. receive
  416. {Self, Events} ->
  417. switch_protocol_flush_h:validate(Events)
  418. after 5000 ->
  419. error(timeout)
  420. end
  421. after
  422. cowboy:stop_listener(?FUNCTION_NAME)
  423. end.
  424. graceful_shutdown_connection(Config) ->
  425. doc("Check that the current request is handled before gracefully "
  426. "shutting down a connection."),
  427. Dispatch = cowboy_router:compile([{"localhost", [
  428. {"/delay_hello", delay_hello_h,
  429. #{delay => 500, notify_received => self()}},
  430. {"/long_delay_hello", delay_hello_h,
  431. #{delay => 10000, notify_received => self()}}
  432. ]}]),
  433. ProtoOpts = #{
  434. env => #{dispatch => Dispatch}
  435. },
  436. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
  437. Port = ranch:get_port(?FUNCTION_NAME),
  438. try
  439. ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  440. {ok, http} = gun:await_up(ConnPid),
  441. #{socket := Socket} = gun:info(ConnPid),
  442. CowboyConnPid = get_remote_pid_tcp(Socket),
  443. CowboyConnRef = erlang:monitor(process, CowboyConnPid),
  444. Ref1 = gun:get(ConnPid, "/delay_hello"),
  445. Ref2 = gun:get(ConnPid, "/delay_hello"),
  446. receive {request_received, <<"/delay_hello">>} -> ok end,
  447. receive {request_received, <<"/delay_hello">>} -> ok end,
  448. ok = sys:terminate(CowboyConnPid, system_is_going_down),
  449. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref1),
  450. <<"close">> = proplists:get_value(<<"connection">>, RespHeaders),
  451. {ok, RespBody} = gun:await_body(ConnPid, Ref1),
  452. <<"Hello world!">> = iolist_to_binary(RespBody),
  453. {error, {stream_error, _}} = gun:await(ConnPid, Ref2),
  454. ok = gun_down(ConnPid),
  455. receive
  456. {'DOWN', CowboyConnRef, process, CowboyConnPid, _Reason} ->
  457. ok
  458. end
  459. after
  460. cowboy:stop_listener(?FUNCTION_NAME)
  461. end.
  462. graceful_shutdown_listener(Config) ->
  463. doc("Check that connections are shut down gracefully when stopping a listener."),
  464. Dispatch = cowboy_router:compile([{"localhost", [
  465. {"/delay_hello", delay_hello_h,
  466. #{delay => 500, notify_received => self()}},
  467. {"/long_delay_hello", delay_hello_h,
  468. #{delay => 10000, notify_received => self()}}
  469. ]}]),
  470. ProtoOpts = #{
  471. env => #{dispatch => Dispatch}
  472. },
  473. {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
  474. Port = ranch:get_port(?FUNCTION_NAME),
  475. ConnPid1 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  476. Ref1 = gun:get(ConnPid1, "/delay_hello"),
  477. ConnPid2 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
  478. Ref2 = gun:get(ConnPid2, "/long_delay_hello"),
  479. %% Shutdown listener while the handlers are working.
  480. receive {request_received, <<"/delay_hello">>} -> ok end,
  481. receive {request_received, <<"/long_delay_hello">>} -> ok end,
  482. ok = cowboy:stop_listener(?FUNCTION_NAME),
  483. %% Check that the 1st request is handled before shutting down.
  484. {response, nofin, 200, RespHeaders} = gun:await(ConnPid1, Ref1),
  485. <<"close">> = proplists:get_value(<<"connection">>, RespHeaders),
  486. {ok, RespBody} = gun:await_body(ConnPid1, Ref1),
  487. <<"Hello world!">> = iolist_to_binary(RespBody),
  488. gun:close(ConnPid1),
  489. %% Check that the 2nd (very slow) request is not handled.
  490. {error, {stream_error, closed}} = gun:await(ConnPid2, Ref2),
  491. gun:close(ConnPid2).