req_SUITE.erl 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261
  1. %% Copyright (c) 2016-2017, 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(req_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(cowboy_test, [gun_open/1]).
  20. %% ct.
  21. suite() ->
  22. Timeout = case os:type() of
  23. {win32, _} -> 120000;
  24. _ -> 30000
  25. end,
  26. [{timetrap, Timeout}].
  27. all() ->
  28. cowboy_test:common_all().
  29. groups() ->
  30. cowboy_test:common_groups(ct_helper:all(?MODULE)).
  31. init_per_suite(Config) ->
  32. ct_helper:create_static_dir(config(priv_dir, Config) ++ "/static"),
  33. Config.
  34. end_per_suite(Config) ->
  35. ct_helper:delete_static_dir(config(priv_dir, Config) ++ "/static").
  36. init_per_group(Name, Config) ->
  37. cowboy_test:init_common_groups(Name, Config, ?MODULE).
  38. end_per_group(Name, _) ->
  39. cowboy:stop_listener(Name).
  40. %% Routes.
  41. init_dispatch(Config) ->
  42. cowboy_router:compile([{"[...]", [
  43. {"/static/[...]", cowboy_static, {dir, config(priv_dir, Config) ++ "/static"}},
  44. %% @todo Seriously InitialState should be optional.
  45. {"/resp/:key[/:arg]", resp_h, []},
  46. {"/multipart[/:key]", multipart_h, []},
  47. {"/args/:key/:arg[/:default]", echo_h, []},
  48. {"/crash/:key/period", echo_h, #{length => 999999999, period => 1000, crash => true}},
  49. {"/no-opts/:key", echo_h, #{crash => true}},
  50. {"/opts/:key/length", echo_h, #{length => 1000}},
  51. {"/opts/:key/period", echo_h, #{length => 999999999, period => 2000}},
  52. {"/opts/:key/timeout", echo_h, #{timeout => 1000, crash => true}},
  53. {"/100-continue/:key", echo_h, []},
  54. {"/full/:key", echo_h, []},
  55. {"/spawn/:key", echo_h, []},
  56. {"/no/:key", echo_h, []},
  57. {"/direct/:key/[...]", echo_h, []},
  58. {"/:key/[...]", echo_h, []}
  59. ]}]).
  60. %% Internal.
  61. do_body(Method, Path, Config) ->
  62. do_body(Method, Path, [], Config).
  63. do_body(Method, Path, Headers, Config) ->
  64. do_body(Method, Path, Headers, <<>>, Config).
  65. do_body(Method, Path, Headers0, Body, Config) ->
  66. ConnPid = gun_open(Config),
  67. Headers = [{<<"accept-encoding">>, <<"gzip">>}|Headers0],
  68. Ref = gun:request(ConnPid, Method, Path, Headers, Body),
  69. {response, IsFin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  70. {ok, RespBody} = case IsFin of
  71. nofin -> gun:await_body(ConnPid, Ref, infinity);
  72. fin -> {ok, <<>>}
  73. end,
  74. gun:close(ConnPid),
  75. do_decode(RespHeaders, RespBody).
  76. do_body_error(Method, Path, Headers0, Body, Config) ->
  77. ConnPid = gun_open(Config),
  78. Headers = [{<<"accept-encoding">>, <<"gzip">>}|Headers0],
  79. Ref = gun:request(ConnPid, Method, Path, Headers, Body),
  80. {response, _, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  81. gun:close(ConnPid),
  82. {Status, RespHeaders}.
  83. do_get(Path, Config) ->
  84. do_get(Path, [], Config).
  85. do_get(Path, Headers, Config) ->
  86. ConnPid = gun_open(Config),
  87. Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
  88. {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  89. {ok, RespBody} = case IsFin of
  90. nofin -> gun:await_body(ConnPid, Ref, infinity);
  91. fin -> {ok, <<>>}
  92. end,
  93. gun:close(ConnPid),
  94. {Status, RespHeaders, do_decode(RespHeaders, RespBody)}.
  95. do_get_body(Path, Config) ->
  96. do_get_body(Path, [], Config).
  97. do_get_body(Path, Headers, Config) ->
  98. do_body("GET", Path, Headers, Config).
  99. do_get_inform(Path, Config) ->
  100. ConnPid = gun_open(Config),
  101. Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]),
  102. case gun:await(ConnPid, Ref, infinity) of
  103. {response, _, RespStatus, RespHeaders} ->
  104. %% We don't care about the body.
  105. gun:close(ConnPid),
  106. {RespStatus, RespHeaders};
  107. {inform, InfoStatus, InfoHeaders} ->
  108. {response, IsFin, RespStatus, RespHeaders}
  109. = case gun:await(ConnPid, Ref, infinity) of
  110. {inform, InfoStatus, InfoHeaders} ->
  111. gun:await(ConnPid, Ref, infinity);
  112. Response ->
  113. Response
  114. end,
  115. {ok, RespBody} = case IsFin of
  116. nofin -> gun:await_body(ConnPid, Ref, infinity);
  117. fin -> {ok, <<>>}
  118. end,
  119. gun:close(ConnPid),
  120. {InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)}
  121. end.
  122. do_decode(Headers, Body) ->
  123. case lists:keyfind(<<"content-encoding">>, 1, Headers) of
  124. {_, <<"gzip">>} -> zlib:gunzip(Body);
  125. _ -> Body
  126. end.
  127. do_get_error(Path, Config) ->
  128. do_get_error(Path, [], Config).
  129. do_get_error(Path, Headers, Config) ->
  130. ConnPid = gun_open(Config),
  131. Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
  132. {response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  133. Result = case IsFin of
  134. nofin -> gun:await_body(ConnPid, Ref, infinity);
  135. fin -> {ok, <<>>}
  136. end,
  137. case Result of
  138. {ok, RespBody} -> {Status, RespHeaders, do_decode(RespHeaders, RespBody)};
  139. _ -> Result
  140. end.
  141. %% Tests: Request.
  142. binding(Config) ->
  143. doc("Value bound from request URI path with/without default."),
  144. <<"binding">> = do_get_body("/args/binding/key", Config),
  145. <<"binding">> = do_get_body("/args/binding/key/default", Config),
  146. <<"default">> = do_get_body("/args/binding/undefined/default", Config),
  147. ok.
  148. bindings(Config) ->
  149. doc("Values bound from request URI path."),
  150. <<"#{key => <<\"bindings\">>}">> = do_get_body("/bindings", Config),
  151. ok.
  152. cert(Config) ->
  153. case config(type, Config) of
  154. tcp -> doc("TLS certificates can only be provided over TLS.");
  155. ssl -> do_cert(Config);
  156. quic -> {skip, "Implement using quicer:peercert/1."}
  157. end.
  158. do_cert(Config) ->
  159. doc("A client TLS certificate was provided."),
  160. Cert = do_get_body("/cert", Config),
  161. Cert = do_get_body("/direct/cert", Config),
  162. ok.
  163. cert_undefined(Config) ->
  164. doc("No client TLS certificate was provided."),
  165. <<"undefined">> = do_get_body("/cert", [{no_cert, true}|Config]),
  166. <<"undefined">> = do_get_body("/direct/cert", [{no_cert, true}|Config]),
  167. ok.
  168. header(Config) ->
  169. doc("Request header with/without default."),
  170. <<"value">> = do_get_body("/args/header/defined", [{<<"defined">>, "value"}], Config),
  171. <<"value">> = do_get_body("/args/header/defined/default", [{<<"defined">>, "value"}], Config),
  172. <<"default">> = do_get_body("/args/header/undefined/default", [{<<"defined">>, "value"}], Config),
  173. ok.
  174. headers(Config) ->
  175. doc("Request headers."),
  176. do_headers("/headers", Config),
  177. do_headers("/direct/headers", Config).
  178. do_headers(Path, Config) ->
  179. %% We always send accept-encoding with this test suite's requests.
  180. <<"#{<<\"accept-encoding\">> => <<\"gzip\">>,"
  181. "<<\"content-length\">> => <<\"0\">>,"
  182. "<<\"header\">> => <<\"value\">>", _/bits>>
  183. = do_get_body(Path, [{<<"header">>, "value"}], Config),
  184. ok.
  185. host(Config) ->
  186. doc("Request URI host."),
  187. <<"localhost">> = do_get_body("/host", Config),
  188. <<"localhost">> = do_get_body("/direct/host", Config),
  189. ok.
  190. host_info(Config) ->
  191. doc("Request host_info."),
  192. <<"[<<\"localhost\">>]">> = do_get_body("/host_info", Config),
  193. ok.
  194. %% @todo Actually write the related unit tests.
  195. match_cookies(Config) ->
  196. doc("Matched request cookies."),
  197. <<"#{}">> = do_get_body("/match/cookies", [{<<"cookie">>, "a=b; c=d"}], Config),
  198. <<"#{a => <<\"b\">>}">> = do_get_body("/match/cookies/a", [{<<"cookie">>, "a=b; c=d"}], Config),
  199. <<"#{c => <<\"d\">>}">> = do_get_body("/match/cookies/c", [{<<"cookie">>, "a=b; c=d"}], Config),
  200. case do_get_body("/match/cookies/a/c", [{<<"cookie">>, "a=b; c=d"}], Config) of
  201. <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok;
  202. <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok
  203. end,
  204. %% Ensure match errors result in a 400 response.
  205. {400, _, _} = do_get("/match/cookies/a/c",
  206. [{<<"cookie">>, "a=b"}], Config),
  207. %% This function is tested more extensively through unit tests.
  208. ok.
  209. %% @todo Actually write the related unit tests.
  210. match_qs(Config) ->
  211. doc("Matched request URI query string."),
  212. <<"#{}">> = do_get_body("/match/qs?a=b&c=d", Config),
  213. <<"#{a => <<\"b\">>}">> = do_get_body("/match/qs/a?a=b&c=d", Config),
  214. <<"#{c => <<\"d\">>}">> = do_get_body("/match/qs/c?a=b&c=d", Config),
  215. case do_get_body("/match/qs/a/c?a=b&c=d", Config) of
  216. <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok;
  217. <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok
  218. end,
  219. case do_get_body("/match/qs/a/c?a=b&c", Config) of
  220. <<"#{a => <<\"b\">>,c => true}">> -> ok;
  221. <<"#{c => true,a => <<\"b\">>}">> -> ok
  222. end,
  223. case do_get_body("/match/qs/a/c?a&c=d", Config) of
  224. <<"#{a => true,c => <<\"d\">>}">> -> ok;
  225. <<"#{c => <<\"d\">>,a => true}">> -> ok
  226. end,
  227. %% Ensure match errors result in a 400 response.
  228. {400, _, _} = do_get("/match/qs/a/c?a=b", [], Config),
  229. %% This function is tested more extensively through unit tests.
  230. ok.
  231. method(Config) ->
  232. doc("Request method."),
  233. do_method("/method", Config),
  234. do_method("/direct/method", Config).
  235. do_method(Path, Config) ->
  236. <<"GET">> = do_body("GET", Path, Config),
  237. <<>> = do_body("HEAD", Path, Config),
  238. <<"OPTIONS">> = do_body("OPTIONS", Path, Config),
  239. <<"PATCH">> = do_body("PATCH", Path, Config),
  240. <<"POST">> = do_body("POST", Path, Config),
  241. <<"PUT">> = do_body("PUT", Path, Config),
  242. <<"ZZZZZZZZ">> = do_body("ZZZZZZZZ", Path, Config),
  243. ok.
  244. parse_cookies(Config) ->
  245. doc("Request cookies."),
  246. <<"[]">> = do_get_body("/parse_cookies", Config),
  247. <<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
  248. = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config),
  249. <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
  250. = do_get_body("/parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config),
  251. <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
  252. = do_get_body("/parse_cookies",
  253. [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config),
  254. %% Ensure parse errors result in a 400 response.
  255. {400, _, _} = do_get("/parse_cookies",
  256. [{<<"cookie">>, "bad\tname=strawberry"}], Config),
  257. {400, _, _} = do_get("/parse_cookies",
  258. [{<<"cookie">>, "goodname=strawberry\tmilkshake"}], Config),
  259. ok.
  260. filter_then_parse_cookies(Config) ->
  261. doc("Filter cookies then parse them."),
  262. <<"[]">> = do_get_body("/filter_then_parse_cookies", Config),
  263. <<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
  264. = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config),
  265. <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
  266. = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config),
  267. <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">>
  268. = do_get_body("/filter_then_parse_cookies",
  269. [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config),
  270. <<"[]">>
  271. = do_get_body("/filter_then_parse_cookies",
  272. [{<<"cookie">>, "bad name=strawberry"}], Config),
  273. <<"[{<<\"cake\">>,<<\"strawberry\">>}]">>
  274. = do_get_body("/filter_then_parse_cookies",
  275. [{<<"cookie">>, "bad name=strawberry; cake=strawberry"}], Config),
  276. <<"[]">>
  277. = do_get_body("/filter_then_parse_cookies",
  278. [{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config),
  279. ok.
  280. parse_header(Config) ->
  281. doc("Parsed request header with/without default."),
  282. <<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">>
  283. = do_get_body("/args/parse_header/accept", [{<<"accept">>, "text/html"}], Config),
  284. <<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">>
  285. = do_get_body("/args/parse_header/accept/default", [{<<"accept">>, "text/html"}], Config),
  286. %% Header not in request but with default defined by Cowboy.
  287. <<"0">> = do_get_body("/args/parse_header/content-length", Config),
  288. %% Header not in request and no default from Cowboy.
  289. <<"undefined">> = do_get_body("/args/parse_header/upgrade", Config),
  290. %% Header in request and with default provided.
  291. <<"100-continue">> = do_get_body("/args/parse_header/expect/100-continue", Config),
  292. %% Ensure parse errors result in a 400 response.
  293. {400, _, _} = do_get("/args/parse_header/accept",
  294. [{<<"accept">>, "bad media type"}], Config),
  295. ok.
  296. parse_qs(Config) ->
  297. doc("Parsed request URI query string."),
  298. <<"[]">> = do_get_body("/parse_qs", Config),
  299. <<"[{<<\"abc\">>,true}]">> = do_get_body("/parse_qs?abc", Config),
  300. <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">> = do_get_body("/parse_qs?a=b&c=d+e", Config),
  301. %% Ensure parse errors result in a 400 response.
  302. {400, _, _} = do_get("/parse_qs?%%%%%%%", Config),
  303. ok.
  304. path(Config) ->
  305. doc("Request URI path."),
  306. do_path("/path", Config),
  307. do_path("/direct/path", Config).
  308. do_path(Path0, Config) ->
  309. Path = list_to_binary(Path0 ++ "/to/the/resource"),
  310. Path = do_get_body(Path, Config),
  311. Path = do_get_body([Path, "?query"], Config),
  312. Path = do_get_body([Path, "?query#fragment"], Config),
  313. Path = do_get_body([Path, "#fragment"], Config),
  314. ok.
  315. path_info(Config) ->
  316. doc("Request path_info."),
  317. <<"undefined">> = do_get_body("/no/path_info", Config),
  318. <<"[]">> = do_get_body("/path_info", Config),
  319. <<"[]">> = do_get_body("/path_info/", Config),
  320. <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource", Config),
  321. <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource?query", Config),
  322. <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource?query#fragment", Config),
  323. <<"[<<\"to\">>,<<\"the\">>,<<\"resource\">>]">> = do_get_body("/path_info/to/the/resource#fragment", Config),
  324. ok.
  325. peer(Config) ->
  326. doc("Remote socket address."),
  327. <<"{{127,0,0,1},", _/bits >> = do_get_body("/peer", Config),
  328. <<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/peer", Config),
  329. ok.
  330. port(Config) ->
  331. doc("Request URI port."),
  332. Port = integer_to_binary(config(port, Config)),
  333. Port = do_get_body("/port", Config),
  334. Port = do_get_body("/direct/port", Config),
  335. ExpectedPort = case config(type, Config) of
  336. tcp -> <<"80">>;
  337. ssl -> <<"443">>;
  338. quic -> <<"443">>
  339. end,
  340. ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config),
  341. ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config),
  342. ok.
  343. qs(Config) ->
  344. doc("Request URI query string."),
  345. do_qs("/qs", Config),
  346. do_qs("/direct/qs", Config).
  347. do_qs(Path, Config) ->
  348. <<>> = do_get_body(Path, Config),
  349. <<"abc">> = do_get_body(Path ++ "?abc", Config),
  350. <<"a=b&c=d+e">> = do_get_body(Path ++ "?a=b&c=d+e", Config),
  351. ok.
  352. scheme(Config) ->
  353. doc("Request URI scheme."),
  354. do_scheme("/scheme", Config),
  355. do_scheme("/direct/scheme", Config).
  356. do_scheme(Path, Config) ->
  357. Transport = config(type, Config),
  358. case do_get_body(Path, Config) of
  359. <<"http">> when Transport =:= tcp -> ok;
  360. <<"https">> when Transport =:= ssl -> ok;
  361. <<"https">> when Transport =:= quic -> ok
  362. end.
  363. sock(Config) ->
  364. doc("Local socket address."),
  365. <<"{{127,0,0,1},", _/bits >> = do_get_body("/sock", Config),
  366. <<"{{127,0,0,1},", _/bits >> = do_get_body("/direct/sock", Config),
  367. ok.
  368. uri(Config) ->
  369. doc("Request URI building/modification."),
  370. Scheme = case config(type, Config) of
  371. tcp -> <<"http">>;
  372. ssl -> <<"https">>;
  373. quic -> <<"https">>
  374. end,
  375. SLen = byte_size(Scheme),
  376. Port = integer_to_binary(config(port, Config)),
  377. PLen = byte_size(Port),
  378. %% Absolute form.
  379. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary, "/uri?qs" >>
  380. = do_get_body("/uri?qs", Config),
  381. %% Origin form.
  382. << "/uri/origin?qs" >> = do_get_body("/uri/origin?qs", Config),
  383. %% Protocol relative.
  384. << "//localhost:", Port:PLen/binary, "/uri/protocol-relative?qs" >>
  385. = do_get_body("/uri/protocol-relative?qs", Config),
  386. %% No query string.
  387. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary, "/uri/no-qs" >>
  388. = do_get_body("/uri/no-qs?qs", Config),
  389. %% No path or query string.
  390. << Scheme:SLen/binary, "://localhost:", Port:PLen/binary >>
  391. = do_get_body("/uri/no-path?qs", Config),
  392. %% Changed port.
  393. << Scheme:SLen/binary, "://localhost:123/uri/set-port?qs" >>
  394. = do_get_body("/uri/set-port?qs", Config),
  395. %% This function is tested more extensively through unit tests.
  396. ok.
  397. version(Config) ->
  398. doc("Request HTTP version."),
  399. do_version("/version", Config),
  400. do_version("/direct/version", Config).
  401. do_version(Path, Config) ->
  402. Protocol = config(protocol, Config),
  403. case do_get_body(Path, Config) of
  404. <<"HTTP/1.1">> when Protocol =:= http -> ok;
  405. <<"HTTP/2">> when Protocol =:= http2 -> ok;
  406. <<"HTTP/3">> when Protocol =:= http3 -> ok
  407. end.
  408. %% Tests: Request body.
  409. body_length(Config) ->
  410. doc("Request body length."),
  411. <<"0">> = do_get_body("/body_length", Config),
  412. <<"12">> = do_body("POST", "/body_length", [], "hello world!", Config),
  413. ok.
  414. has_body(Config) ->
  415. doc("Has a request body?"),
  416. <<"false">> = do_get_body("/has_body", Config),
  417. <<"true">> = do_body("POST", "/has_body", [], "hello world!", Config),
  418. ok.
  419. read_body(Config) ->
  420. doc("Request body."),
  421. <<>> = do_get_body("/read_body", Config),
  422. <<"hello world!">> = do_body("POST", "/read_body", [], "hello world!", Config),
  423. %% We expect to have read *at least* 1000 bytes.
  424. <<0:8000, _/bits>> = do_body("POST", "/opts/read_body/length", [], <<0:8000000>>, Config),
  425. %% The timeout value is set too low on purpose to ensure a crash occurs.
  426. ok = do_read_body_timeout("/opts/read_body/timeout", <<0:8000000>>, Config),
  427. %% 10MB body larger than default length.
  428. <<0:80000000>> = do_body("POST", "/full/read_body", [], <<0:80000000>>, Config),
  429. ok.
  430. read_body_mtu(Config) ->
  431. case os:type() of
  432. {win32, _} ->
  433. {skip, "Loopback MTU size is 0xFFFFFFFF on Windows."};
  434. {unix, _} ->
  435. doc("Request body whose sizes are around the MTU."),
  436. MTU = ct_helper:get_loopback_mtu(),
  437. _ = [begin
  438. Body = <<0:Size/unit:8>>,
  439. Body = do_body("POST", "/full/read_body", [], Body, Config)
  440. end || Size <- lists:seq(MTU - 10, MTU + 10)],
  441. ok
  442. end.
  443. read_body_period(Config) ->
  444. doc("Read the request body for at most 2 seconds."),
  445. ConnPid = gun_open(Config),
  446. Body = <<0:8000000>>,
  447. Ref = gun:headers(ConnPid, "POST", "/opts/read_body/period", [
  448. {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
  449. ]),
  450. %% The body is sent without fin. The server will read what it can
  451. %% for 2 seconds. The test succeeds if we get some of the data back
  452. %% (meaning the function will have returned after the period ends).
  453. gun:data(ConnPid, Ref, nofin, Body),
  454. {response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
  455. {data, _, Data} = gun:await(ConnPid, Ref, infinity),
  456. %% We expect to read at least some data.
  457. true = Data =/= <<>>,
  458. gun:close(ConnPid).
  459. %% We expect a crash.
  460. do_read_body_timeout(Path, Body, Config) ->
  461. ConnPid = gun_open(Config),
  462. Ref = gun:headers(ConnPid, "POST", Path, [
  463. {<<"content-length">>, integer_to_binary(byte_size(Body))}
  464. ]),
  465. {response, _, 500, _} = gun:await(ConnPid, Ref, infinity),
  466. gun:close(ConnPid).
  467. read_body_spawn(Config) ->
  468. doc("Confirm we can use cowboy_req:read_body/1,2 from another process."),
  469. <<"hello world!">> = do_body("POST", "/spawn/read_body", [], "hello world!", Config),
  470. ok.
  471. read_body_expect_100_continue(Config) ->
  472. doc("Request body with a 100-continue expect header."),
  473. do_read_body_expect_100_continue("/read_body", Config).
  474. read_body_expect_100_continue_user_sent(Config) ->
  475. doc("Request body with a 100-continue expect header, 100 response sent by handler."),
  476. do_read_body_expect_100_continue("/100-continue/read_body", Config).
  477. do_read_body_expect_100_continue(Path, Config) ->
  478. ConnPid = gun_open(Config),
  479. Body = <<0:8000000>>,
  480. Headers = [
  481. {<<"accept-encoding">>, <<"gzip">>},
  482. {<<"expect">>, <<"100-continue">>},
  483. {<<"content-length">>, integer_to_binary(byte_size(Body))}
  484. ],
  485. Ref = gun:post(ConnPid, Path, Headers),
  486. {inform, 100, []} = gun:await(ConnPid, Ref, infinity),
  487. gun:data(ConnPid, Ref, fin, Body),
  488. {response, IsFin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  489. {ok, RespBody} = case IsFin of
  490. nofin -> gun:await_body(ConnPid, Ref, infinity);
  491. fin -> {ok, <<>>}
  492. end,
  493. gun:close(ConnPid),
  494. do_decode(RespHeaders, RespBody).
  495. read_urlencoded_body(Config) ->
  496. doc("application/x-www-form-urlencoded request body."),
  497. <<"[]">> = do_body("POST", "/read_urlencoded_body", [], <<>>, Config),
  498. <<"[{<<\"abc\">>,true}]">> = do_body("POST", "/read_urlencoded_body", [], "abc", Config),
  499. <<"[{<<\"a\">>,<<\"b\">>},{<<\"c\">>,<<\"d e\">>}]">>
  500. = do_body("POST", "/read_urlencoded_body", [], "a=b&c=d+e", Config),
  501. %% The timeout value is set too low on purpose to ensure a crash occurs.
  502. ok = do_read_body_timeout("/opts/read_urlencoded_body/timeout", <<"abc">>, Config),
  503. %% Ensure parse errors result in a 400 response.
  504. {400, _} = do_body_error("POST", "/read_urlencoded_body", [], "%%%%%", Config),
  505. ok.
  506. read_urlencoded_body_too_large(Config) ->
  507. doc("application/x-www-form-urlencoded request body too large. "
  508. "Send a 10MB body, larger than the default length, to ensure a crash occurs."),
  509. do_read_urlencoded_body_too_large("/no-opts/read_urlencoded_body",
  510. string:chars($a, 10000000), Config).
  511. %% We expect a crash.
  512. do_read_urlencoded_body_too_large(Path, Body, Config) ->
  513. ConnPid = gun_open(Config),
  514. Ref = gun:headers(ConnPid, "POST", Path, [
  515. {<<"content-length">>, integer_to_binary(iolist_size(Body))}
  516. ]),
  517. gun:data(ConnPid, Ref, fin, Body),
  518. {response, _, 413, _} = gun:await(ConnPid, Ref, infinity),
  519. gun:close(ConnPid).
  520. read_urlencoded_body_too_long(Config) ->
  521. doc("application/x-www-form-urlencoded request body sent too slow. "
  522. "The body is simply not being sent fully. It is read by the handler "
  523. "for at most 1 second. A crash occurs because we don't have the full body."),
  524. do_read_urlencoded_body_too_long("/crash/read_urlencoded_body/period", <<"abc">>, Config).
  525. %% We expect a crash.
  526. do_read_urlencoded_body_too_long(Path, Body, Config) ->
  527. ConnPid = gun_open(Config),
  528. Ref = gun:headers(ConnPid, "POST", Path, [
  529. {<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
  530. ]),
  531. gun:data(ConnPid, Ref, nofin, Body),
  532. {response, _, 408, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  533. _ = case config(protocol, Config) of
  534. http ->
  535. %% 408 error responses should close HTTP/1.1 connections.
  536. {_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders);
  537. http2 ->
  538. ok;
  539. http3 ->
  540. ok
  541. end,
  542. gun:close(ConnPid).
  543. read_and_match_urlencoded_body(Config) ->
  544. doc("Read and match an application/x-www-form-urlencoded request body."),
  545. <<"#{}">> = do_body("POST", "/match/body_qs", [], "a=b&c=d", Config),
  546. <<"#{a => <<\"b\">>}">> = do_body("POST", "/match/body_qs/a", [], "a=b&c=d", Config),
  547. <<"#{c => <<\"d\">>}">> = do_body("POST", "/match/body_qs/c", [], "a=b&c=d", Config),
  548. case do_body("POST", "/match/body_qs/a/c", [], "a=b&c=d", Config) of
  549. <<"#{a => <<\"b\">>,c => <<\"d\">>}">> -> ok;
  550. <<"#{c => <<\"d\">>,a => <<\"b\">>}">> -> ok
  551. end,
  552. case do_body("POST", "/match/body_qs/a/c", [], "a=b&c", Config) of
  553. <<"#{a => <<\"b\">>,c => true}">> -> ok;
  554. <<"#{c => true,a => <<\"b\">>}">> -> ok
  555. end,
  556. case do_body("POST", "/match/body_qs/a/c", [], "a&c=d", Config) of
  557. <<"#{a => true,c => <<\"d\">>}">> -> ok;
  558. <<"#{c => <<\"d\">>,a => true}">> -> ok
  559. end,
  560. %% Ensure match errors result in a 400 response.
  561. {400, _} = do_body_error("POST", "/match/body_qs/a/c", [], "a=b", Config),
  562. %% Ensure parse errors result in a 400 response.
  563. {400, _} = do_body_error("POST", "/match/body_qs", [], "%%%%%", Config),
  564. %% The timeout value is set too low on purpose to ensure a crash occurs.
  565. ok = do_read_body_timeout("/opts/read_and_match_urlencoded_body/timeout", <<"abc">>, Config),
  566. ok.
  567. read_and_match_urlencoded_body_too_large(Config) ->
  568. doc("Read and match an application/x-www-form-urlencoded request body too large. "
  569. "Send a 10MB body, larger than the default length, to ensure a crash occurs."),
  570. do_read_urlencoded_body_too_large(
  571. "/no-opts/read_and_match_urlencoded_body",
  572. string:chars($a, 10000000), Config).
  573. read_and_match_urlencoded_body_too_long(Config) ->
  574. doc("Read and match an application/x-www-form-urlencoded request body sent too slow. "
  575. "The body is simply not being sent fully. It is read by the handler "
  576. "for at most 1 second. A crash occurs because we don't have the full body."),
  577. do_read_urlencoded_body_too_long(
  578. "/crash/read_and_match_urlencoded_body/period", <<"abc">>, Config).
  579. multipart(Config) ->
  580. doc("Multipart request body."),
  581. do_multipart("/multipart", Config).
  582. do_multipart(Path, Config) ->
  583. LargeBody = iolist_to_binary(string:chars($a, 10000000)),
  584. ReqBody = [
  585. "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n"
  586. "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n"
  587. "--deadbeef--"
  588. ],
  589. RespBody = do_body("POST", Path, [
  590. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  591. ], ReqBody, Config),
  592. [
  593. {#{<<"content-type">> := <<"text/plain">>}, <<"Cowboy is an HTTP server.">>},
  594. {LargeHeaders, LargeBody}
  595. ] = binary_to_term(RespBody),
  596. #{
  597. <<"content-type">> := <<"application/octet-stream">>,
  598. <<"x-custom">> := <<"value">>
  599. } = LargeHeaders,
  600. ok.
  601. multipart_error_empty(Config) ->
  602. doc("Multipart request body is empty."),
  603. %% We use an empty list as a body to make sure Gun knows
  604. %% we want to send an empty body.
  605. %% @todo This is a terrible hack. Improve Gun!
  606. Body = [],
  607. %% Ensure an empty body results in a 400 error.
  608. {400, _} = do_body_error("POST", "/multipart", [
  609. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  610. ], Body, Config),
  611. ok.
  612. multipart_error_preamble_only(Config) ->
  613. doc("Multipart request body only contains a preamble."),
  614. %% Ensure an empty body results in a 400 error.
  615. {400, _} = do_body_error("POST", "/multipart", [
  616. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  617. ], <<"Preamble.">>, Config),
  618. ok.
  619. multipart_error_headers(Config) ->
  620. doc("Multipart request body with invalid part headers."),
  621. ReqBody = [
  622. "--deadbeef\r\nbad-header text/plain\r\n\r\nCowboy is an HTTP server.\r\n"
  623. "--deadbeef--"
  624. ],
  625. %% Ensure parse errors result in a 400 response.
  626. {400, _} = do_body_error("POST", "/multipart", [
  627. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  628. ], ReqBody, Config),
  629. ok.
  630. %% The function to parse the multipart body currently does not crash,
  631. %% as far as I can tell. There is therefore no test for it.
  632. multipart_error_no_final_boundary(Config) ->
  633. doc("Multipart request body with no final boundary."),
  634. ReqBody = [
  635. "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n"
  636. ],
  637. %% Ensure parse errors result in a 400 response.
  638. {400, _} = do_body_error("POST", "/multipart", [
  639. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  640. ], ReqBody, Config),
  641. ok.
  642. multipart_missing_boundary(Config) ->
  643. doc("Multipart request body without a boundary in the media type."),
  644. ReqBody = [
  645. "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n"
  646. "--deadbeef--"
  647. ],
  648. %% Ensure parse errors result in a 400 response.
  649. {400, _} = do_body_error("POST", "/multipart", [
  650. {<<"content-type">>, <<"multipart/mixed">>}
  651. ], ReqBody, Config),
  652. ok.
  653. read_part_skip_body(Config) ->
  654. doc("Multipart request body skipping part bodies."),
  655. LargeBody = iolist_to_binary(string:chars($a, 10000000)),
  656. ReqBody = [
  657. "--deadbeef\r\nContent-Type: text/plain\r\n\r\nCowboy is an HTTP server.\r\n"
  658. "--deadbeef\r\nContent-Type: application/octet-stream\r\nX-Custom: value\r\n\r\n", LargeBody, "\r\n"
  659. "--deadbeef--"
  660. ],
  661. RespBody = do_body("POST", "/multipart/skip_body", [
  662. {<<"content-type">>, <<"multipart/mixed; boundary=deadbeef">>}
  663. ], ReqBody, Config),
  664. [
  665. #{<<"content-type">> := <<"text/plain">>},
  666. LargeHeaders
  667. ] = binary_to_term(RespBody),
  668. #{
  669. <<"content-type">> := <<"application/octet-stream">>,
  670. <<"x-custom">> := <<"value">>
  671. } = LargeHeaders,
  672. ok.
  673. %% @todo When reading a multipart body, length and period
  674. %% only apply to a single read_body call. We may want a
  675. %% separate option to know how many reads we want to do
  676. %% before we give up.
  677. read_part2(Config) ->
  678. doc("Multipart request body using read_part/2."),
  679. %% Override the length and period values only, making
  680. %% the request process use more read_body calls.
  681. %%
  682. %% We do not try a custom timeout value since this would
  683. %% be the same test as read_body/2.
  684. do_multipart("/multipart/read_part2", Config).
  685. read_part_body2(Config) ->
  686. doc("Multipart request body using read_part_body/2."),
  687. %% Override the length and period values only, making
  688. %% the request process use more read_body calls.
  689. %%
  690. %% We do not try a custom timeout value since this would
  691. %% be the same test as read_body/2.
  692. do_multipart("/multipart/read_part_body2", Config).
  693. %% Tests: Response.
  694. %% @todo We want to crash when calling set_resp_* or related
  695. %% functions after the reply has been sent.
  696. set_resp_cookie(Config) ->
  697. doc("Response using set_resp_cookie."),
  698. %% Single cookie, no options.
  699. {200, Headers1, _} = do_get("/resp/set_resp_cookie3", Config),
  700. {_, <<"mycookie=myvalue">>}
  701. = lists:keyfind(<<"set-cookie">>, 1, Headers1),
  702. %% Single cookie, with options.
  703. {200, Headers2, _} = do_get("/resp/set_resp_cookie4", Config),
  704. {_, <<"mycookie=myvalue; Path=/resp/set_resp_cookie4">>}
  705. = lists:keyfind(<<"set-cookie">>, 1, Headers2),
  706. %% Multiple cookies.
  707. {200, Headers3, _} = do_get("/resp/set_resp_cookie3/multiple", Config),
  708. [_, _] = [H || H={<<"set-cookie">>, _} <- Headers3],
  709. %% Overwrite previously set cookie.
  710. {200, Headers4, _} = do_get("/resp/set_resp_cookie3/overwrite", Config),
  711. {_, <<"mycookie=overwrite">>}
  712. = lists:keyfind(<<"set-cookie">>, 1, Headers4),
  713. ok.
  714. set_resp_header(Config) ->
  715. doc("Response using set_resp_header."),
  716. {200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config),
  717. true = lists:keymember(<<"content-type">>, 1, Headers),
  718. ok.
  719. set_resp_headers(Config) ->
  720. doc("Response using set_resp_headers."),
  721. {200, Headers, <<"OK">>} = do_get("/resp/set_resp_headers", Config),
  722. true = lists:keymember(<<"content-type">>, 1, Headers),
  723. true = lists:keymember(<<"content-encoding">>, 1, Headers),
  724. ok.
  725. resp_header(Config) ->
  726. doc("Response header with/without default."),
  727. {200, _, <<"OK">>} = do_get("/resp/resp_header_defined", Config),
  728. {200, _, <<"OK">>} = do_get("/resp/resp_header_default", Config),
  729. ok.
  730. resp_headers(Config) ->
  731. doc("Get all response headers."),
  732. {200, _, <<"OK">>} = do_get("/resp/resp_headers", Config),
  733. {200, _, <<"OK">>} = do_get("/resp/resp_headers_empty", Config),
  734. ok.
  735. set_resp_body(Config) ->
  736. doc("Response using set_resp_body."),
  737. {200, _, <<"OK">>} = do_get("/resp/set_resp_body", Config),
  738. {200, _, <<"OVERRIDE">>} = do_get("/resp/set_resp_body/override", Config),
  739. {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")),
  740. {200, _, AppFile} = do_get("/resp/set_resp_body/sendfile", Config),
  741. ok.
  742. set_resp_body_sendfile0(Config) ->
  743. doc("Response using set_resp_body with a sendfile of length 0."),
  744. Path = "/resp/set_resp_body/sendfile0",
  745. ConnPid = gun_open(Config),
  746. %% First request.
  747. Ref1 = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]),
  748. {response, IsFin, 200, _} = gun:await(ConnPid, Ref1, infinity),
  749. {ok, <<>>} = case IsFin of
  750. nofin -> gun:await_body(ConnPid, Ref1, infinity);
  751. fin -> {ok, <<>>}
  752. end,
  753. %% Second request will confirm everything works as intended.
  754. Ref2 = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}]),
  755. {response, IsFin, 200, _} = gun:await(ConnPid, Ref2, infinity),
  756. {ok, <<>>} = case IsFin of
  757. nofin -> gun:await_body(ConnPid, Ref2, infinity);
  758. fin -> {ok, <<>>}
  759. end,
  760. gun:close(ConnPid),
  761. ok.
  762. has_resp_header(Config) ->
  763. doc("Has response header?"),
  764. {200, Headers, <<"OK">>} = do_get("/resp/has_resp_header", Config),
  765. true = lists:keymember(<<"content-type">>, 1, Headers),
  766. ok.
  767. has_resp_body(Config) ->
  768. doc("Has response body?"),
  769. {200, _, <<"OK">>} = do_get("/resp/has_resp_body", Config),
  770. {200, _, <<"OK">>} = do_get("/resp/has_resp_body/sendfile", Config),
  771. ok.
  772. delete_resp_header(Config) ->
  773. doc("Delete response header."),
  774. {200, Headers, <<"OK">>} = do_get("/resp/delete_resp_header", Config),
  775. false = lists:keymember(<<"content-type">>, 1, Headers),
  776. ok.
  777. inform2(Config) ->
  778. doc("Informational response(s) without headers, followed by the real response."),
  779. {102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config),
  780. {102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
  781. {500, _} = do_get_inform("/resp/inform2/error", Config),
  782. {102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config),
  783. ok.
  784. inform3(Config) ->
  785. doc("Informational response(s) with headers, followed by the real response."),
  786. Headers = [{<<"ext-header">>, <<"ext-value">>}],
  787. {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
  788. {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
  789. {500, _} = do_get_inform("/resp/inform3/error", Config),
  790. {102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config),
  791. ok.
  792. reply2(Config) ->
  793. doc("Response with default headers and no body."),
  794. {200, _, _} = do_get("/resp/reply2/200", Config),
  795. {201, _, _} = do_get("/resp/reply2/201", Config),
  796. {404, _, _} = do_get("/resp/reply2/404", Config),
  797. {200, _, _} = do_get("/resp/reply2/binary", Config),
  798. {500, _, _} = do_get("/resp/reply2/error", Config),
  799. %% @todo We want to crash when reply or stream_reply is called twice.
  800. %% How to test this properly? This isn't enough.
  801. {200, _, _} = do_get("/resp/reply2/twice", Config),
  802. ok.
  803. reply3(Config) ->
  804. doc("Response with additional headers and no body."),
  805. {200, Headers1, _} = do_get("/resp/reply3/200", Config),
  806. true = lists:keymember(<<"content-type">>, 1, Headers1),
  807. {201, Headers2, _} = do_get("/resp/reply3/201", Config),
  808. true = lists:keymember(<<"content-type">>, 1, Headers2),
  809. {404, Headers3, _} = do_get("/resp/reply3/404", Config),
  810. true = lists:keymember(<<"content-type">>, 1, Headers3),
  811. {500, _, _} = do_get("/resp/reply3/error", Config),
  812. ok.
  813. reply4(Config) ->
  814. doc("Response with additional headers and body."),
  815. {200, _, <<"OK">>} = do_get("/resp/reply4/200", Config),
  816. {201, _, <<"OK">>} = do_get("/resp/reply4/201", Config),
  817. {404, _, <<"OK">>} = do_get("/resp/reply4/404", Config),
  818. {500, _, _} = do_get("/resp/reply4/error", Config),
  819. ok.
  820. %% @todo Crash when stream_reply is called twice.
  821. stream_reply2(Config) ->
  822. doc("Response with default headers and streamed body."),
  823. Body = <<0:8000000>>,
  824. {200, _, Body} = do_get("/resp/stream_reply2/200", Config),
  825. {201, _, Body} = do_get("/resp/stream_reply2/201", Config),
  826. {404, _, Body} = do_get("/resp/stream_reply2/404", Config),
  827. {200, _, Body} = do_get("/resp/stream_reply2/binary", Config),
  828. {500, _, _} = do_get("/resp/stream_reply2/error", Config),
  829. ok.
  830. stream_reply3(Config) ->
  831. doc("Response with additional headers and streamed body."),
  832. Body = <<0:8000000>>,
  833. {200, Headers1, Body} = do_get("/resp/stream_reply3/200", Config),
  834. true = lists:keymember(<<"content-type">>, 1, Headers1),
  835. {201, Headers2, Body} = do_get("/resp/stream_reply3/201", Config),
  836. true = lists:keymember(<<"content-type">>, 1, Headers2),
  837. {404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config),
  838. true = lists:keymember(<<"content-type">>, 1, Headers3),
  839. {500, _, _} = do_get("/resp/stream_reply3/error", Config),
  840. ok.
  841. stream_body_fin0(Config) ->
  842. doc("Streamed body with last chunk of size 0."),
  843. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/fin0", Config),
  844. ok.
  845. stream_body_multiple(Config) ->
  846. doc("Streamed body via multiple calls."),
  847. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/multiple", Config),
  848. ok.
  849. stream_body_loop(Config) ->
  850. doc("Streamed body via a fast loop."),
  851. {200, _, <<0:32000000/unit:8>>} = do_get("/resp/stream_body/loop", Config),
  852. ok.
  853. stream_body_nofin(Config) ->
  854. doc("Unfinished streamed body."),
  855. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/nofin", Config),
  856. ok.
  857. stream_body_sendfile(Config) ->
  858. doc("Streamed body via multiple calls, including sendfile calls."),
  859. {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")),
  860. ExpectedBody = iolist_to_binary([
  861. <<"Hello ">>,
  862. AppFile,
  863. <<" interspersed ">>,
  864. AppFile,
  865. <<" world!">>
  866. ]),
  867. {200, _, ExpectedBody} = do_get("/resp/stream_body/sendfile", Config),
  868. ok.
  869. stream_body_sendfile_fin(Config) ->
  870. doc("Streamed body via multiple calls, including a sendfile final call."),
  871. {ok, AppFile} = file:read_file(code:where_is_file("cowboy.app")),
  872. ExpectedBody = iolist_to_binary([
  873. <<"Hello! ">>,
  874. AppFile
  875. ]),
  876. {200, _, ExpectedBody} = do_get("/resp/stream_body/sendfile_fin", Config),
  877. ok.
  878. stream_body_spawn(Config) ->
  879. doc("Confirm we can use cowboy_req:stream_body/3 from another process."),
  880. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body/spawn", Config),
  881. ok.
  882. stream_body_content_length_multiple(Config) ->
  883. doc("Streamed body via multiple calls."),
  884. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/multiple", Config),
  885. ok.
  886. stream_body_content_length_fin0(Config) ->
  887. doc("Streamed body with last chunk of size 0."),
  888. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/fin0", Config),
  889. ok.
  890. stream_body_content_length_nofin(Config) ->
  891. doc("Unfinished streamed body."),
  892. {200, _, <<"Hello world!">>} = do_get("/resp/stream_body_content_length/nofin", Config),
  893. ok.
  894. stream_body_content_length_nofin_error(Config) ->
  895. doc("Not all of the response body sent."),
  896. case config(protocol, Config) of
  897. http ->
  898. case do_get_error("/resp/stream_body_content_length/nofin-error", Config) of
  899. %% When compression is used content-length is not sent.
  900. {200, Headers, <<"Hello">>} ->
  901. {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers);
  902. %% The server closes the connection when the body couldn't be sent fully.
  903. {error, {stream_error, closed}} ->
  904. receive
  905. {gun_down, ConnPid, _, _, _} ->
  906. gun:close(ConnPid)
  907. after 1000 ->
  908. error(timeout)
  909. end
  910. end;
  911. http2 ->
  912. %% @todo HTTP/2 should have the same content-length checks.
  913. {skip, "Implement the test for HTTP/2."};
  914. http3 ->
  915. %% @todo HTTP/3 should have the same content-length checks.
  916. {skip, "Implement the test for HTTP/3."}
  917. end.
  918. stream_body_concurrent(Config) ->
  919. ConnPid = gun_open(Config),
  920. Ref1 = gun:get(ConnPid, "/resp/stream_body/loop", [{<<"accept-encoding">>, <<"gzip">>}]),
  921. Ref2 = gun:get(ConnPid, "/resp/stream_body/loop", [{<<"accept-encoding">>, <<"gzip">>}]),
  922. {response, nofin, 200, _} = gun:await(ConnPid, Ref1, infinity),
  923. {ok, _} = gun:await_body(ConnPid, Ref1, infinity),
  924. {response, nofin, 200, _} = gun:await(ConnPid, Ref2, infinity),
  925. {ok, _} = gun:await_body(ConnPid, Ref2, infinity),
  926. gun:close(ConnPid).
  927. %% @todo Crash when calling stream_body after the fin flag has been set.
  928. %% @todo Crash when calling stream_body after calling reply.
  929. %% @todo Crash when calling stream_body before calling stream_reply.
  930. stream_events_single(Config) ->
  931. doc("Streamed event."),
  932. {200, Headers, <<
  933. "event: add_comment\n"
  934. "data: Comment text.\n"
  935. "data: With many lines.\n"
  936. "\n"
  937. >>} = do_get("/resp/stream_events/single", Config),
  938. {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  939. ok.
  940. stream_events_list(Config) ->
  941. doc("Streamed list of events."),
  942. {200, Headers, <<
  943. "event: add_comment\n"
  944. "data: Comment text.\n"
  945. "data: With many lines.\n"
  946. "\n"
  947. ": Set retry higher\n"
  948. ": with many lines also.\n"
  949. "retry: 10000\n"
  950. "\n"
  951. "id: 123\n"
  952. "event: add_comment\n"
  953. "data: Closing!\n"
  954. "\n"
  955. >>} = do_get("/resp/stream_events/list", Config),
  956. {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  957. ok.
  958. stream_events_multiple(Config) ->
  959. doc("Streamed events via multiple calls."),
  960. {200, Headers, <<
  961. "event: add_comment\n"
  962. "data: Comment text.\n"
  963. "data: With many lines.\n"
  964. "\n"
  965. ": Set retry higher\n"
  966. ": with many lines also.\n"
  967. "retry: 10000\n"
  968. "\n"
  969. "id: 123\n"
  970. "event: add_comment\n"
  971. "data: Closing!\n"
  972. "\n"
  973. >>} = do_get("/resp/stream_events/multiple", Config),
  974. {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  975. ok.
  976. stream_trailers(Config) ->
  977. doc("Stream body followed by trailer headers."),
  978. {200, RespHeaders, <<"Hello world!">>, [
  979. {<<"grpc-status">>, <<"0">>}
  980. ]} = do_trailers("/resp/stream_trailers", Config),
  981. {_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders),
  982. ok.
  983. stream_trailers_large(Config) ->
  984. doc("Stream large body followed by trailer headers."),
  985. {200, RespHeaders, <<0:80000000>>, [
  986. {<<"grpc-status">>, <<"0">>}
  987. ]} = do_trailers("/resp/stream_trailers/large", Config),
  988. {_, <<"grpc-status">>} = lists:keyfind(<<"trailer">>, 1, RespHeaders),
  989. ok.
  990. stream_trailers_no_te(Config) ->
  991. doc("Stream body followed by trailer headers without a te header in the request."),
  992. ConnPid = gun_open(Config),
  993. Ref = gun:get(ConnPid, "/resp/stream_trailers", [
  994. {<<"accept-encoding">>, <<"gzip">>}
  995. ]),
  996. {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  997. %% @todo Do we want to remove the trailer header automatically?
  998. % false = lists:keyfind(<<"trailer">>, 1, RespHeaders),
  999. {ok, RespBody} = gun:await_body(ConnPid, Ref, infinity),
  1000. <<"Hello world!">> = do_decode(RespHeaders, RespBody),
  1001. gun:close(ConnPid).
  1002. do_trailers(Path, Config) ->
  1003. ConnPid = gun_open(Config),
  1004. Ref = gun:get(ConnPid, Path, [
  1005. {<<"accept-encoding">>, <<"gzip">>},
  1006. {<<"te">>, <<"trailers">>}
  1007. ]),
  1008. {response, nofin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
  1009. {ok, RespBody, Trailers} = gun:await_body(ConnPid, Ref, infinity),
  1010. gun:close(ConnPid),
  1011. {Status, RespHeaders, do_decode(RespHeaders, RespBody), Trailers}.
  1012. %% @todo Crash when calling stream_trailers twice.
  1013. %% @todo Crash when calling stream_trailers after the fin flag has been set.
  1014. %% @todo Crash when calling stream_trailers after calling reply.
  1015. %% @todo Crash when calling stream_trailers before calling stream_reply.
  1016. %% Tests: Push.
  1017. %% @todo We want to crash when push is called after reply has been initiated.
  1018. push(Config) ->
  1019. case config(protocol, Config) of
  1020. http -> do_push_http("/resp/push", Config);
  1021. http2 -> do_push_http2(Config);
  1022. http3 -> {skip, "Implement server push for HTTP/3."}
  1023. end.
  1024. push_method(Config) ->
  1025. case config(protocol, Config) of
  1026. http -> do_push_http("/resp/push/method", Config);
  1027. http2 -> do_push_http2_method(Config);
  1028. http3 -> {skip, "Implement server push for HTTP/3."}
  1029. end.
  1030. push_origin(Config) ->
  1031. case config(protocol, Config) of
  1032. http -> do_push_http("/resp/push/origin", Config);
  1033. http2 -> do_push_http2_origin(Config);
  1034. http3 -> {skip, "Implement server push for HTTP/3."}
  1035. end.
  1036. push_qs(Config) ->
  1037. case config(protocol, Config) of
  1038. http -> do_push_http("/resp/push/qs", Config);
  1039. http2 -> do_push_http2_qs(Config);
  1040. http3 -> {skip, "Implement server push for HTTP/3."}
  1041. end.
  1042. do_push_http(Path, Config) ->
  1043. doc("Ignore pushed responses when protocol is HTTP/1.1."),
  1044. ConnPid = gun_open(Config),
  1045. Ref = gun:get(ConnPid, Path, []),
  1046. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
  1047. ok.
  1048. do_push_http2(Config) ->
  1049. doc("Pushed responses."),
  1050. ConnPid = gun_open(Config),
  1051. Ref = gun:get(ConnPid, "/resp/push", []),
  1052. %% We expect two pushed resources.
  1053. Origin = iolist_to_binary([
  1054. case config(type, Config) of
  1055. tcp -> "http";
  1056. ssl -> "https"
  1057. end,
  1058. "://localhost:",
  1059. integer_to_binary(config(port, Config))
  1060. ]),
  1061. OriginLen = byte_size(Origin),
  1062. {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css">>,
  1063. [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity),
  1064. {push, PushTXT, <<"GET">>, <<Origin:OriginLen/binary, "/static/plain.txt">>,
  1065. [{<<"accept">>,<<"text/plain">>}]} = gun:await(ConnPid, Ref, infinity),
  1066. %% Pushed CSS.
  1067. {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity),
  1068. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS),
  1069. {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity),
  1070. %% Pushed TXT is 406 because the pushed accept header uses an undefined type.
  1071. {response, fin, 406, _} = gun:await(ConnPid, PushTXT, infinity),
  1072. %% Let's not forget about the response to the client's request.
  1073. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
  1074. gun:close(ConnPid).
  1075. do_push_http2_method(Config) ->
  1076. doc("Pushed response with non-GET method."),
  1077. ConnPid = gun_open(Config),
  1078. Ref = gun:get(ConnPid, "/resp/push/method", []),
  1079. %% Pushed CSS.
  1080. {push, PushCSS, <<"HEAD">>, _, [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity),
  1081. {response, fin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity),
  1082. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS),
  1083. %% Let's not forget about the response to the client's request.
  1084. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
  1085. gun:close(ConnPid).
  1086. do_push_http2_origin(Config) ->
  1087. doc("Pushed response with custom scheme/host/port."),
  1088. ConnPid = gun_open(Config),
  1089. Ref = gun:get(ConnPid, "/resp/push/origin", []),
  1090. %% Pushed CSS.
  1091. {push, PushCSS, <<"GET">>, <<"ftp://127.0.0.1:21/static/style.css">>,
  1092. [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity),
  1093. {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity),
  1094. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS),
  1095. {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity),
  1096. %% Let's not forget about the response to the client's request.
  1097. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
  1098. gun:close(ConnPid).
  1099. do_push_http2_qs(Config) ->
  1100. doc("Pushed response with query string."),
  1101. ConnPid = gun_open(Config),
  1102. Ref = gun:get(ConnPid, "/resp/push/qs", []),
  1103. %% Pushed CSS.
  1104. Origin = iolist_to_binary([
  1105. case config(type, Config) of
  1106. tcp -> "http";
  1107. ssl -> "https"
  1108. end,
  1109. "://localhost:",
  1110. integer_to_binary(config(port, Config))
  1111. ]),
  1112. OriginLen = byte_size(Origin),
  1113. {push, PushCSS, <<"GET">>, <<Origin:OriginLen/binary, "/static/style.css?server=cowboy&version=2.0">>,
  1114. [{<<"accept">>,<<"text/css">>}]} = gun:await(ConnPid, Ref, infinity),
  1115. {response, nofin, 200, HeadersCSS} = gun:await(ConnPid, PushCSS, infinity),
  1116. {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, HeadersCSS),
  1117. {ok, <<"body{color:red}\n">>} = gun:await_body(ConnPid, PushCSS, infinity),
  1118. %% Let's not forget about the response to the client's request.
  1119. {response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
  1120. gun:close(ConnPid).