rest_handler_SUITE.erl 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. %% Copyright (c) 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(rest_handler_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. all() ->
  22. cowboy_test:common_all().
  23. groups() ->
  24. cowboy_test:common_groups(ct_helper:all(?MODULE)).
  25. init_per_group(Name, Config) ->
  26. cowboy_test:init_common_groups(Name, Config, ?MODULE).
  27. end_per_group(Name, _) ->
  28. cowboy:stop_listener(Name).
  29. %% Dispatch configuration.
  30. init_dispatch(_) ->
  31. cowboy_router:compile([{'_', [
  32. {"/", rest_hello_h, []},
  33. {"/charsets_provided", charsets_provided_h, []},
  34. {"/charsets_provided_empty", charsets_provided_empty_h, []},
  35. {"/charset_in_content_types_provided",
  36. charset_in_content_types_provided_h, []},
  37. {"/charset_in_content_types_provided_implicit",
  38. charset_in_content_types_provided_implicit_h, []},
  39. {"/charset_in_content_types_provided_implicit_no_callback",
  40. charset_in_content_types_provided_implicit_no_callback_h, []},
  41. {"/if_range", if_range_h, []},
  42. {"/provide_callback_missing", provide_callback_missing_h, []},
  43. {"/provide_range_callback", provide_range_callback_h, []},
  44. {"/range_satisfiable", range_satisfiable_h, []},
  45. {"/ranges_provided", ranges_provided_h, []},
  46. {"/ranges_provided_auto", ranges_provided_auto_h, []},
  47. {"/rate_limited", rate_limited_h, []},
  48. {"/stop_handler", stop_handler_h, []},
  49. {"/switch_handler", switch_handler_h, run},
  50. {"/switch_handler_opts", switch_handler_h, hibernate}
  51. ]}]).
  52. %% Internal.
  53. do_decode(Headers, Body) ->
  54. case lists:keyfind(<<"content-encoding">>, 1, Headers) of
  55. {_, <<"gzip">>} -> zlib:gunzip(Body);
  56. _ -> Body
  57. end.
  58. %% Tests.
  59. error_on_malformed_if_match(Config) ->
  60. doc("A malformed If-Match header must result in a 400 response."),
  61. ConnPid = gun_open(Config),
  62. Ref = gun:get(ConnPid, "/", [
  63. {<<"accept-encoding">>, <<"gzip">>},
  64. {<<"if-match">>, <<"bad">>}
  65. ]),
  66. {response, _, 400, _} = gun:await(ConnPid, Ref),
  67. ok.
  68. error_on_malformed_if_none_match(Config) ->
  69. doc("A malformed If-None-Match header must result in a 400 response."),
  70. ConnPid = gun_open(Config),
  71. Ref = gun:get(ConnPid, "/", [
  72. {<<"accept-encoding">>, <<"gzip">>},
  73. {<<"if-none-match">>, <<"bad">>}
  74. ]),
  75. {response, _, 400, _} = gun:await(ConnPid, Ref),
  76. ok.
  77. charset_in_content_types_provided(Config) ->
  78. doc("When a charset is matched explictly in content_types_provided, "
  79. "that charset is used and the charsets_provided callback is ignored."),
  80. ConnPid = gun_open(Config),
  81. Ref = gun:get(ConnPid, "/charset_in_content_types_provided", [
  82. {<<"accept">>, <<"text/plain;charset=utf-8">>},
  83. {<<"accept-charset">>, <<"utf-16, utf-8;q=0.5">>},
  84. {<<"accept-encoding">>, <<"gzip">>}
  85. ]),
  86. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  87. {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  88. ok.
  89. charset_in_content_types_provided_implicit_match(Config) ->
  90. doc("When a charset is matched implicitly in content_types_provided, "
  91. "the charsets_provided callback is used to determine if the media "
  92. "type will match."),
  93. ConnPid = gun_open(Config),
  94. Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [
  95. {<<"accept">>, <<"text/plain;charset=utf-16">>},
  96. {<<"accept-encoding">>, <<"gzip">>}
  97. ]),
  98. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  99. {_, <<"text/plain; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  100. ok.
  101. charset_in_content_types_provided_implicit_nomatch(Config) ->
  102. doc("When a charset is matched implicitly in content_types_provided, "
  103. "the charsets_provided callback is used to determine if the media "
  104. "type will match. If it doesn't, try the next media type."),
  105. ConnPid = gun_open(Config),
  106. Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [
  107. {<<"accept">>, <<"text/plain;charset=utf-32, text/plain">>},
  108. {<<"accept-encoding">>, <<"gzip">>}
  109. ]),
  110. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  111. %% We end up with the first charset listed in charsets_provided.
  112. {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  113. ok.
  114. charset_in_content_types_provided_implicit_nomatch_error(Config) ->
  115. doc("When a charset is matched implicitly in content_types_provided, "
  116. "the charsets_provided callback is used to determine if the media "
  117. "type will match. If it doesn't, and there's no other media type, "
  118. "a 406 is returned."),
  119. ConnPid = gun_open(Config),
  120. Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit", [
  121. {<<"accept">>, <<"text/plain;charset=utf-32">>},
  122. {<<"accept-encoding">>, <<"gzip">>}
  123. ]),
  124. {response, _, 406, _} = gun:await(ConnPid, Ref),
  125. ok.
  126. charset_in_content_types_provided_implicit_no_callback(Config) ->
  127. doc("When a charset is matched implicitly in content_types_provided, "
  128. "and the charsets_provided callback is not exported, the media "
  129. "type will match but the charset will be ignored like all other "
  130. "parameters."),
  131. ConnPid = gun_open(Config),
  132. Ref = gun:get(ConnPid, "/charset_in_content_types_provided_implicit_no_callback", [
  133. {<<"accept">>, <<"text/plain;charset=utf-32">>},
  134. {<<"accept-encoding">>, <<"gzip">>}
  135. ]),
  136. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  137. %% The charset is ignored as if it was any other parameter.
  138. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  139. ok.
  140. charsets_provided_match_text(Config) ->
  141. doc("When the media type is text and the charsets_provided callback exists "
  142. "and the accept-charset header was sent, the selected charset is sent "
  143. "back in the content-type of the response."),
  144. ConnPid = gun_open(Config),
  145. Ref = gun:get(ConnPid, "/charsets_provided", [
  146. {<<"accept">>, <<"text/plain">>},
  147. {<<"accept-charset">>, <<"utf-8;q=0.5, utf-16">>},
  148. {<<"accept-encoding">>, <<"gzip">>}
  149. ]),
  150. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  151. {_, <<"text/plain; charset=utf-16">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  152. ok.
  153. charsets_provided_match_other(Config) ->
  154. doc("When the media type is not text and the charsets_provided callback exists "
  155. "and the accept-charset header was sent, the selected charset is not sent "
  156. "back in the content-type of the response."),
  157. ConnPid = gun_open(Config),
  158. Ref = gun:get(ConnPid, "/charsets_provided", [
  159. {<<"accept">>, <<"application/json">>},
  160. {<<"accept-charset">>, <<"utf-8;q=0.5, utf-16">>},
  161. {<<"accept-encoding">>, <<"gzip">>}
  162. ]),
  163. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  164. {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  165. ok.
  166. charsets_provided_wildcard_text(Config) ->
  167. doc("When the media type is text and the charsets_provided callback exists "
  168. "and a wildcard accept-charset header was sent, the selected charset is sent "
  169. "back in the content-type of the response."),
  170. ConnPid = gun_open(Config),
  171. Ref = gun:get(ConnPid, "/charsets_provided", [
  172. {<<"accept">>, <<"text/plain">>},
  173. {<<"accept-charset">>, <<"*">>},
  174. {<<"accept-encoding">>, <<"gzip">>}
  175. ]),
  176. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  177. {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  178. ok.
  179. charsets_provided_wildcard_other(Config) ->
  180. doc("When the media type is not text and the charsets_provided callback exists "
  181. "and a wildcard accept-charset header was sent, the selected charset is not sent "
  182. "back in the content-type of the response."),
  183. ConnPid = gun_open(Config),
  184. Ref = gun:get(ConnPid, "/charsets_provided", [
  185. {<<"accept">>, <<"application/json">>},
  186. {<<"accept-charset">>, <<"*">>},
  187. {<<"accept-encoding">>, <<"gzip">>}
  188. ]),
  189. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  190. {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  191. ok.
  192. charsets_provided_nomatch(Config) ->
  193. doc("Regardless of the media type negotiated, if no charset is found in the "
  194. "accept-charset header match a charset configured in charsets_provided, "
  195. "then a 406 not acceptable response is sent back."),
  196. ConnPid = gun_open(Config),
  197. Ref = gun:get(ConnPid, "/charsets_provided", [
  198. {<<"accept">>, <<"text/plain">>},
  199. {<<"accept-charset">>, <<"utf-8;q=0, iso-8859-1">>},
  200. {<<"accept-encoding">>, <<"gzip">>}
  201. ]),
  202. {response, _, 406, _} = gun:await(ConnPid, Ref),
  203. ok.
  204. charsets_provided_noheader_text(Config) ->
  205. doc("When the media type is text and the charsets_provided callback exists "
  206. "but the accept-charset header was not sent, the first charset in the "
  207. "list is selected and sent back in the content-type of the response."),
  208. ConnPid = gun_open(Config),
  209. Ref = gun:get(ConnPid, "/charsets_provided", [
  210. {<<"accept">>, <<"text/plain">>},
  211. {<<"accept-encoding">>, <<"gzip">>}
  212. ]),
  213. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  214. {_, <<"text/plain; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  215. ok.
  216. charsets_provided_noheader_other(Config) ->
  217. doc("When the media type is not text and the charsets_provided callback exists "
  218. "but the accept-charset header was not sent, the first charset in the "
  219. "list is selected but is not sent back in the content-type of the response."),
  220. ConnPid = gun_open(Config),
  221. Ref = gun:get(ConnPid, "/charsets_provided", [
  222. {<<"accept">>, <<"application/json">>},
  223. {<<"accept-encoding">>, <<"gzip">>}
  224. ]),
  225. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  226. {_, <<"application/json">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  227. ok.
  228. charsets_provided_empty(Config) ->
  229. doc("Regardless of the media type negotiated, if the charsets_provided "
  230. "callback returns an empty list a 406 not acceptable response is sent back."),
  231. ConnPid = gun_open(Config),
  232. Ref = gun:get(ConnPid, "/charsets_provided_empty", [
  233. {<<"accept">>, <<"text/plain">>},
  234. {<<"accept-charset">>, <<"utf-8q=0.5, utf-16">>},
  235. {<<"accept-encoding">>, <<"gzip">>}
  236. ]),
  237. {response, _, 406, _} = gun:await(ConnPid, Ref),
  238. ok.
  239. charsets_provided_empty_wildcard(Config) ->
  240. doc("Regardless of the media type negotiated, if the charsets_provided "
  241. "callback returns an empty list a 406 not acceptable response is sent back."),
  242. ConnPid = gun_open(Config),
  243. Ref = gun:get(ConnPid, "/charsets_provided_empty", [
  244. {<<"accept">>, <<"text/plain">>},
  245. {<<"accept-charset">>, <<"*">>},
  246. {<<"accept-encoding">>, <<"gzip">>}
  247. ]),
  248. {response, _, 406, _} = gun:await(ConnPid, Ref),
  249. ok.
  250. charsets_provided_empty_noheader(Config) ->
  251. doc("Regardless of the media type negotiated, if the charsets_provided "
  252. "callback returns an empty list a 406 not acceptable response is sent back."),
  253. ConnPid = gun_open(Config),
  254. Ref = gun:get(ConnPid, "/charsets_provided_empty", [
  255. {<<"accept">>, <<"text/plain">>},
  256. {<<"accept-encoding">>, <<"gzip">>}
  257. ]),
  258. {response, _, 406, _} = gun:await(ConnPid, Ref),
  259. ok.
  260. if_range_etag_equal(Config) ->
  261. doc("When the if-range header matches, a 206 partial content "
  262. "response is expected for an otherwise valid range request. (RFC7233 3.2)"),
  263. ConnPid = gun_open(Config),
  264. Ref = gun:get(ConnPid, "/if_range", [
  265. {<<"accept-encoding">>, <<"gzip">>},
  266. {<<"range">>, <<"bytes=0-">>},
  267. {<<"if-range">>, <<"\"strong-and-match\"">>}
  268. ]),
  269. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  270. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  271. {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  272. ok.
  273. if_range_etag_not_equal(Config) ->
  274. doc("When the if-range header does not match, the range header "
  275. "must be ignored and a 200 OK response is expected for "
  276. "an otherwise valid range request. (RFC7233 3.2)"),
  277. ConnPid = gun_open(Config),
  278. Ref = gun:get(ConnPid, "/if_range", [
  279. {<<"accept-encoding">>, <<"gzip">>},
  280. {<<"range">>, <<"bytes=0-">>},
  281. {<<"if-range">>, <<"\"strong-but-no-match\"">>}
  282. ]),
  283. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  284. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  285. false = lists:keyfind(<<"content-range">>, 1, Headers),
  286. ok.
  287. if_range_ignored_when_no_range_header(Config) ->
  288. doc("When there is no range header the if-range header is ignored "
  289. "and a 200 OK response is expected (RFC7233 3.2)"),
  290. ConnPid = gun_open(Config),
  291. Ref = gun:get(ConnPid, "/if_range", [
  292. {<<"accept-encoding">>, <<"gzip">>},
  293. {<<"if-range">>, <<"\"strong-and-match\"">>}
  294. ]),
  295. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  296. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  297. false = lists:keyfind(<<"content-range">>, 1, Headers),
  298. ok.
  299. if_range_ignored_when_ranges_provided_missing(Config) ->
  300. doc("When the resource does not support range requests "
  301. "the range and if-range headers must be ignored"
  302. "and a 200 OK response is expected (RFC7233 3.2)"),
  303. ConnPid = gun_open(Config),
  304. Ref = gun:get(ConnPid, "/if_range?missing-ranges_provided", [
  305. {<<"accept-encoding">>, <<"gzip">>},
  306. {<<"range">>, <<"bytes=0-">>},
  307. {<<"if-range">>, <<"\"strong-and-match\"">>}
  308. ]),
  309. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  310. false = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  311. false = lists:keyfind(<<"content-range">>, 1, Headers),
  312. ok.
  313. if_range_ignored_when_ranges_provided_empty(Config) ->
  314. doc("When the resource does not support range requests "
  315. "the range and if-range headers must be ignored"
  316. "and a 200 OK response is expected (RFC7233 3.2)"),
  317. ConnPid = gun_open(Config),
  318. Ref = gun:get(ConnPid, "/if_range?empty-ranges_provided", [
  319. {<<"accept-encoding">>, <<"gzip">>},
  320. {<<"range">>, <<"bytes=0-">>},
  321. {<<"if-range">>, <<"\"strong-and-match\"">>}
  322. ]),
  323. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  324. {_, <<"none">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  325. false = lists:keyfind(<<"content-range">>, 1, Headers),
  326. ok.
  327. if_range_weak_etag_not_equal(Config) ->
  328. doc("The if-range header must not match weak etags; the range header "
  329. "must be ignored and a 200 OK response is expected for "
  330. "an otherwise valid range request. (RFC7233 3.2)"),
  331. ConnPid = gun_open(Config),
  332. Ref = gun:get(ConnPid, "/if_range?weak-etag", [
  333. {<<"accept-encoding">>, <<"gzip">>},
  334. {<<"range">>, <<"bytes=0-">>},
  335. {<<"if-range">>, <<"W/\"weak-no-match\"">>}
  336. ]),
  337. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  338. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  339. false = lists:keyfind(<<"content-range">>, 1, Headers),
  340. ok.
  341. if_range_date_not_equal(Config) ->
  342. doc("The if-range header must not match weak dates. Cowboy "
  343. "currently has no way of knowing whether a resource was "
  344. "updated twice within the same second. The range header "
  345. "must be ignored and a 200 OK response is expected for "
  346. "an otherwise valid range request. (RFC7233 3.2)"),
  347. ConnPid = gun_open(Config),
  348. Ref = gun:get(ConnPid, "/if_range", [
  349. {<<"accept-encoding">>, <<"gzip">>},
  350. {<<"range">>, <<"bytes=0-">>},
  351. {<<"if-range">>, <<"Fri, 22 Feb 2222 11:11:11 GMT">>}
  352. ]),
  353. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  354. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  355. false = lists:keyfind(<<"content-range">>, 1, Headers),
  356. ok.
  357. provide_callback_missing(Config) ->
  358. doc("A 500 response must be sent when the ProvideCallback can't be called."),
  359. ConnPid = gun_open(Config),
  360. Ref = gun:get(ConnPid, "/provide_callback_missing", [{<<"accept-encoding">>, <<"gzip">>}]),
  361. {response, fin, 500, _} = gun:await(ConnPid, Ref),
  362. ok.
  363. provide_range_callback(Config) ->
  364. doc("A successful request for a single range results in a "
  365. "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"),
  366. ConnPid = gun_open(Config),
  367. Ref = gun:get(ConnPid, "/provide_range_callback", [
  368. {<<"accept-encoding">>, <<"gzip">>},
  369. {<<"range">>, <<"bytes=0-">>}
  370. ]),
  371. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  372. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  373. {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  374. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  375. {ok, <<"This is ranged REST!">>} = gun:await_body(ConnPid, Ref),
  376. ok.
  377. provide_range_callback_sendfile(Config) ->
  378. doc("A successful request for a single range results in a "
  379. "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"),
  380. ConnPid = gun_open(Config),
  381. Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [
  382. {<<"accept-encoding">>, <<"gzip">>},
  383. {<<"range">>, <<"bytes=0-">>}
  384. ]),
  385. Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
  386. Size = filelib:file_size(Path),
  387. {ok, Body} = file:read_file(Path),
  388. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  389. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  390. {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers),
  391. ContentRange = iolist_to_binary([
  392. <<"bytes 0-">>,
  393. integer_to_binary(Size - 1),
  394. <<"/">>,
  395. integer_to_binary(Size)
  396. ]),
  397. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  398. {ok, Body} = gun:await_body(ConnPid, Ref),
  399. ok.
  400. provide_range_callback_multipart(Config) ->
  401. doc("A successful request for multiple ranges results in a "
  402. "206 partial content response using the multipart/byteranges "
  403. "content-type and the content-range not being set. The real "
  404. "content-type and content-range of the parts can be found in "
  405. "the multipart headers. (RFC7233 4.1, RFC7233 A)"),
  406. ConnPid = gun_open(Config),
  407. Ref = gun:get(ConnPid, "/provide_range_callback", [
  408. {<<"accept-encoding">>, <<"gzip">>},
  409. %% This range selects everything except the space characters.
  410. {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>}
  411. ]),
  412. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  413. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  414. false = lists:keyfind(<<"content-range">>, 1, Headers),
  415. {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
  416. = lists:keyfind(<<"content-type">>, 1, Headers),
  417. {ok, Body0} = gun:await_body(ConnPid, Ref),
  418. Body = do_decode(Headers, Body0),
  419. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
  420. [
  421. {bytes, 0, 3, 20},
  422. {bytes, 5, 6, 20},
  423. {bytes, 8, 13, 20},
  424. {bytes, 15, 19, 20}
  425. ] = ContentRanges,
  426. <<"ThisisrangedREST!">> = BodyAcc,
  427. ok.
  428. provide_range_callback_multipart_sendfile(Config) ->
  429. doc("A successful request for multiple ranges results in a "
  430. "206 partial content response using the multipart/byteranges "
  431. "content-type and the content-range not being set. The real "
  432. "content-type and content-range of the parts can be found in "
  433. "the multipart headers. (RFC7233 4.1, RFC7233 A)"),
  434. ConnPid = gun_open(Config),
  435. Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [
  436. {<<"accept-encoding">>, <<"gzip">>},
  437. %% This range selects a few random chunks of the file.
  438. {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>}
  439. ]),
  440. Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
  441. Size = filelib:file_size(Path),
  442. Skip = Size - 399,
  443. {ok, <<
  444. _:50/binary, Body1:50/binary,
  445. _:50/binary, Body2:50/binary,
  446. _:50/binary, Body3:50/binary,
  447. _:Skip/binary, Body4/bits>>} = file:read_file(Path),
  448. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  449. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  450. false = lists:keyfind(<<"content-range">>, 1, Headers),
  451. {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
  452. = lists:keyfind(<<"content-type">>, 1, Headers),
  453. {ok, Body0} = gun:await_body(ConnPid, Ref),
  454. Body = do_decode(Headers, Body0),
  455. %% We will receive the ranges in the same order as requested.
  456. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
  457. LastFrom = 300 + Skip,
  458. LastTo = Size - 1,
  459. [
  460. {bytes, 50, 99, Size},
  461. {bytes, 150, 199, Size},
  462. {bytes, 250, 299, Size},
  463. {bytes, LastFrom, LastTo, Size}
  464. ] = ContentRanges,
  465. BodyAcc = <<Body1/binary, Body2/binary, Body3/binary, Body4/binary>>,
  466. ok.
  467. do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) ->
  468. case cow_multipart:parse_headers(Rest, Boundary) of
  469. {ok, Headers, Rest1} ->
  470. {_, ContentRange0} = lists:keyfind(<<"content-range">>, 1, Headers),
  471. ContentRange = cow_http_hd:parse_content_range(ContentRange0),
  472. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  473. case cow_multipart:parse_body(Rest1, Boundary) of
  474. {done, Body} ->
  475. do_provide_range_callback_multipart_body(<<>>, Boundary,
  476. [ContentRange|ContentRangesAcc],
  477. <<BodyAcc/binary, Body/binary>>);
  478. {done, Body, Rest2} ->
  479. do_provide_range_callback_multipart_body(Rest2, Boundary,
  480. [ContentRange|ContentRangesAcc],
  481. <<BodyAcc/binary, Body/binary>>)
  482. end;
  483. {done, <<>>} ->
  484. {lists:reverse(ContentRangesAcc), BodyAcc}
  485. end.
  486. provide_range_callback_metadata(Config) ->
  487. doc("A successful request for a single range results in a "
  488. "206 partial content response with the same headers that "
  489. "a normal 200 OK response would, like vary or etag. (RFC7233 4.1)"),
  490. ConnPid = gun_open(Config),
  491. Ref = gun:get(ConnPid, "/provide_range_callback", [
  492. {<<"accept-encoding">>, <<"gzip">>},
  493. {<<"range">>, <<"bytes=0-">>}
  494. ]),
  495. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  496. {_, _} = lists:keyfind(<<"date">>, 1, Headers),
  497. {_, _} = lists:keyfind(<<"etag">>, 1, Headers),
  498. {_, _} = lists:keyfind(<<"expires">>, 1, Headers),
  499. {_, _} = lists:keyfind(<<"last-modified">>, 1, Headers),
  500. {_, _} = lists:keyfind(<<"vary">>, 1, Headers),
  501. %% Also cache-control and content-location but we don't send those.
  502. ok.
  503. provide_range_callback_missing(Config) ->
  504. doc("A 500 response must be sent when the ProvideRangeCallback can't be called."),
  505. ConnPid = gun_open(Config),
  506. Ref = gun:get(ConnPid, "/provide_range_callback?missing", [
  507. {<<"accept-encoding">>, <<"gzip">>},
  508. {<<"range">>, <<"bytes=0-">>}
  509. ]),
  510. {response, fin, 500, _} = gun:await(ConnPid, Ref),
  511. ok.
  512. range_ignore_unknown_unit(Config) ->
  513. doc("The range header must be ignored when the range unit "
  514. "is not found in ranges_provided. (RFC7233 3.1)"),
  515. ConnPid = gun_open(Config),
  516. Ref = gun:get(ConnPid, "/if_range", [
  517. {<<"accept-encoding">>, <<"gzip">>},
  518. {<<"range">>, <<"chapters=1-">>}
  519. ]),
  520. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  521. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  522. false = lists:keyfind(<<"content-range">>, 1, Headers),
  523. ok.
  524. range_ignore_when_not_modified(Config) ->
  525. doc("The range header must be ignored when a conditional "
  526. "GET results in a 304 not modified response. (RFC7233 3.1)"),
  527. ConnPid = gun_open(Config),
  528. Ref = gun:get(ConnPid, "/if_range", [
  529. {<<"accept-encoding">>, <<"gzip">>},
  530. {<<"range">>, <<"bytes=0-">>},
  531. {<<"if-none-match">>, <<"\"strong-and-match\"">>}
  532. ]),
  533. {response, fin, 304, Headers} = gun:await(ConnPid, Ref),
  534. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  535. false = lists:keyfind(<<"content-range">>, 1, Headers),
  536. ok.
  537. range_satisfiable(Config) ->
  538. doc("When the range_satisfiable callback returns true "
  539. "a 206 partial content response is expected for "
  540. "an otherwise valid range request. (RFC7233 4.1)"),
  541. ConnPid = gun_open(Config),
  542. Ref = gun:get(ConnPid, "/range_satisfiable?true", [
  543. {<<"accept-encoding">>, <<"gzip">>},
  544. {<<"range">>, <<"bytes=0-">>}
  545. ]),
  546. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  547. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  548. {_, <<"bytes 0-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  549. ok.
  550. range_not_satisfiable(Config) ->
  551. doc("When the range_satisfiable callback returns false "
  552. "a 416 range not satisfiable response is expected for "
  553. "an otherwise valid range request. (RFC7233 4.4)"),
  554. ConnPid = gun_open(Config),
  555. Ref = gun:get(ConnPid, "/range_satisfiable?false", [
  556. {<<"accept-encoding">>, <<"gzip">>},
  557. {<<"range">>, <<"bytes=0-">>}
  558. ]),
  559. {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
  560. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  561. false = lists:keyfind(<<"content-range">>, 1, Headers),
  562. ok.
  563. range_not_satisfiable_int(Config) ->
  564. doc("When the range_satisfiable callback returns false "
  565. "a 416 range not satisfiable response is expected for "
  566. "an otherwise valid range request. If an integer is "
  567. "provided it is used to construct the content-range "
  568. "header. (RFC7233 4.2, RFC7233 4.4)"),
  569. ConnPid = gun_open(Config),
  570. Ref = gun:get(ConnPid, "/range_satisfiable?false-int", [
  571. {<<"accept-encoding">>, <<"gzip">>},
  572. {<<"range">>, <<"bytes=0-">>}
  573. ]),
  574. {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
  575. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  576. {_, <<"bytes */123">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  577. ok.
  578. range_not_satisfiable_bin(Config) ->
  579. doc("When the range_satisfiable callback returns false "
  580. "a 416 range not satisfiable response is expected for "
  581. "an otherwise valid range request. If a binary is "
  582. "provided it is used to construct the content-range "
  583. "header. (RFC7233 4.2, RFC7233 4.4)"),
  584. ConnPid = gun_open(Config),
  585. Ref = gun:get(ConnPid, "/range_satisfiable?false-bin", [
  586. {<<"accept-encoding">>, <<"gzip">>},
  587. {<<"range">>, <<"bytes=0-">>}
  588. ]),
  589. {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
  590. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  591. {_, <<"bytes */456">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  592. ok.
  593. range_satisfiable_missing(Config) ->
  594. doc("When the range_satisfiable callback is missing "
  595. "a 206 partial content response is expected for "
  596. "an otherwise valid range request. (RFC7233 4.1)"),
  597. ConnPid = gun_open(Config),
  598. Ref = gun:get(ConnPid, "/range_satisfiable?missing", [
  599. {<<"accept-encoding">>, <<"gzip">>},
  600. {<<"range">>, <<"bytes=0-">>}
  601. ]),
  602. {response, _, 206, Headers} = gun:await(ConnPid, Ref),
  603. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  604. {_, <<"bytes ", _/bits>>} = lists:keyfind(<<"content-range">>, 1, Headers),
  605. ok.
  606. ranges_provided_accept_ranges(Config) ->
  607. doc("When the ranges_provided callback exists the accept-ranges header "
  608. "is sent in the response. (RFC7233 2.3)"),
  609. ConnPid = gun_open(Config),
  610. Ref = gun:get(ConnPid, "/ranges_provided?list", [{<<"accept-encoding">>, <<"gzip">>}]),
  611. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  612. {_, <<"bytes, pages, chapters">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  613. ok.
  614. %% @todo Probably should have options to do this automatically for auto at least.
  615. %%
  616. %% A server that supports range requests MAY ignore or reject a Range
  617. %% header field that consists of more than two overlapping ranges, or a
  618. %% set of many small ranges that are not listed in ascending order,
  619. %% since both are indications of either a broken client or a deliberate
  620. %% denial-of-service attack (Section 6.1).
  621. %% @todo Probably should have options for auto as well to join ranges that
  622. %% are very close from each other.
  623. ranges_provided_auto_data(Config) ->
  624. doc("When the unit range is bytes and the callback is 'auto' "
  625. "Cowboy will call the normal ProvideCallback and perform "
  626. "the range calculations automatically."),
  627. ConnPid = gun_open(Config),
  628. Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
  629. {<<"accept-encoding">>, <<"gzip">>},
  630. {<<"range">>, <<"bytes=8-">>}
  631. ]),
  632. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  633. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  634. {_, <<"bytes 8-19/20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  635. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  636. {ok, <<"ranged REST!">>} = gun:await_body(ConnPid, Ref),
  637. ok.
  638. ranges_provided_auto_sendfile(Config) ->
  639. doc("When the unit range is bytes and the callback is 'auto' "
  640. "Cowboy will call the normal ProvideCallback and perform "
  641. "the range calculations automatically."),
  642. ConnPid = gun_open(Config),
  643. Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
  644. {<<"accept-encoding">>, <<"gzip">>},
  645. {<<"range">>, <<"bytes=8-">>}
  646. ]),
  647. Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
  648. Size = filelib:file_size(Path),
  649. {ok, <<_:8/binary, Body/bits>>} = file:read_file(Path),
  650. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  651. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  652. {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers),
  653. ContentRange = iolist_to_binary([
  654. <<"bytes 8-">>,
  655. integer_to_binary(Size - 1),
  656. <<"/">>,
  657. integer_to_binary(Size)
  658. ]),
  659. {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  660. {ok, Body} = gun:await_body(ConnPid, Ref),
  661. ok.
  662. ranges_provided_auto_multipart_data(Config) ->
  663. doc("When the unit range is bytes and the callback is 'auto' "
  664. "Cowboy will call the normal ProvideCallback and perform "
  665. "the range calculations automatically."),
  666. ConnPid = gun_open(Config),
  667. Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
  668. {<<"accept-encoding">>, <<"gzip">>},
  669. %% This range selects everything except the space characters.
  670. {<<"range">>, <<"bytes=0-3, 5-6, 8-13, 15-">>}
  671. ]),
  672. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  673. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  674. false = lists:keyfind(<<"content-range">>, 1, Headers),
  675. {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
  676. = lists:keyfind(<<"content-type">>, 1, Headers),
  677. {ok, Body0} = gun:await_body(ConnPid, Ref),
  678. Body = do_decode(Headers, Body0),
  679. %% We will receive the ranges in the same order as requested.
  680. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
  681. [
  682. {bytes, 0, 3, 20},
  683. {bytes, 5, 6, 20},
  684. {bytes, 8, 13, 20},
  685. {bytes, 15, 19, 20}
  686. ] = ContentRanges,
  687. <<"ThisisrangedREST!">> = BodyAcc,
  688. ok.
  689. ranges_provided_auto_multipart_sendfile(Config) ->
  690. doc("When the unit range is bytes and the callback is 'auto' "
  691. "Cowboy will call the normal ProvideCallback and perform "
  692. "the range calculations automatically."),
  693. ConnPid = gun_open(Config),
  694. Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
  695. {<<"accept-encoding">>, <<"gzip">>},
  696. %% This range selects a few random chunks of the file.
  697. {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>}
  698. ]),
  699. Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
  700. Size = filelib:file_size(Path),
  701. Skip = Size - 399,
  702. {ok, <<
  703. _:50/binary, Body1:50/binary,
  704. _:50/binary, Body2:50/binary,
  705. _:50/binary, Body3:50/binary,
  706. _:Skip/binary, Body4/bits>>} = file:read_file(Path),
  707. {response, nofin, 206, Headers} = gun:await(ConnPid, Ref),
  708. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  709. false = lists:keyfind(<<"content-range">>, 1, Headers),
  710. {_, <<"multipart/byteranges; boundary=", Boundary/bits>>}
  711. = lists:keyfind(<<"content-type">>, 1, Headers),
  712. {ok, Body0} = gun:await_body(ConnPid, Ref),
  713. Body = do_decode(Headers, Body0),
  714. %% We will receive the ranges in the same order as requested.
  715. {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>),
  716. LastFrom = 300 + Skip,
  717. LastTo = Size - 1,
  718. [
  719. {bytes, 50, 99, Size},
  720. {bytes, 150, 199, Size},
  721. {bytes, 250, 299, Size},
  722. {bytes, LastFrom, LastTo, Size}
  723. ] = ContentRanges,
  724. BodyAcc = <<Body1/binary, Body2/binary, Body3/binary, Body4/binary>>,
  725. ok.
  726. ranges_provided_auto_not_satisfiable_data(Config) ->
  727. doc("When the unit range is bytes and the callback is 'auto' "
  728. "Cowboy will call the normal ProvideCallback and perform "
  729. "the range calculations automatically. When the requested "
  730. "range is not satisfiable a 416 range not satisfiable response "
  731. "is expected. The content-range header will be set. (RFC7233 4.4)"),
  732. ConnPid = gun_open(Config),
  733. Ref = gun:get(ConnPid, "/ranges_provided_auto?data", [
  734. {<<"accept-encoding">>, <<"gzip">>},
  735. {<<"range">>, <<"bytes=1000-">>}
  736. ]),
  737. {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
  738. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  739. {_, <<"bytes */20">>} = lists:keyfind(<<"content-range">>, 1, Headers),
  740. ok.
  741. ranges_provided_auto_not_satisfiable_sendfile(Config) ->
  742. doc("When the unit range is bytes and the callback is 'auto' "
  743. "Cowboy will call the normal ProvideCallback and perform "
  744. "the range calculations automatically. When the requested "
  745. "range is not satisfiable a 416 range not satisfiable response "
  746. "is expected. The content-range header will be set. (RFC7233 4.4)"),
  747. ConnPid = gun_open(Config),
  748. Ref = gun:get(ConnPid, "/ranges_provided_auto?sendfile", [
  749. {<<"accept-encoding">>, <<"gzip">>},
  750. {<<"range">>, <<"bytes=1000-">>}
  751. ]),
  752. {response, fin, 416, Headers} = gun:await(ConnPid, Ref),
  753. {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  754. Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app",
  755. Size = filelib:file_size(Path),
  756. ContentRange = iolist_to_binary([<<"bytes */">>, integer_to_binary(Size)]),
  757. {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers),
  758. ok.
  759. ranges_provided_empty_accept_ranges_none(Config) ->
  760. doc("When the ranges_provided callback exists but returns an empty list "
  761. "the accept-ranges header is sent in the response with the value none. (RFC7233 2.3)"),
  762. ConnPid = gun_open(Config),
  763. Ref = gun:get(ConnPid, "/ranges_provided?none", [{<<"accept-encoding">>, <<"gzip">>}]),
  764. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  765. {_, <<"none">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  766. ok.
  767. ranges_provided_missing_no_accept_ranges(Config) ->
  768. doc("When the ranges_provided callback does not exist "
  769. "the accept-ranges header is not sent in the response."),
  770. ConnPid = gun_open(Config),
  771. Ref = gun:get(ConnPid, "/ranges_provided?missing", [{<<"accept-encoding">>, <<"gzip">>}]),
  772. {response, _, 200, Headers} = gun:await(ConnPid, Ref),
  773. false = lists:keyfind(<<"accept-ranges">>, 1, Headers),
  774. ok.
  775. rate_limited(Config) ->
  776. doc("A 429 response must be sent when the rate_limited callback returns true. "
  777. "The retry-after header is specified as an integer. (RFC6585 4, RFC7231 7.1.3)"),
  778. ConnPid = gun_open(Config),
  779. Ref = gun:get(ConnPid, "/rate_limited?true", [{<<"accept-encoding">>, <<"gzip">>}]),
  780. {response, fin, 429, Headers} = gun:await(ConnPid, Ref),
  781. {_, <<"3600">>} = lists:keyfind(<<"retry-after">>, 1, Headers),
  782. ok.
  783. rate_limited_datetime(Config) ->
  784. doc("A 429 response must be sent when the rate_limited callback returns true. "
  785. "The retry-after header is specified as a date/time tuple. (RFC6585 4, RFC7231 7.1.3)"),
  786. ConnPid = gun_open(Config),
  787. Ref = gun:get(ConnPid, "/rate_limited?true-date", [{<<"accept-encoding">>, <<"gzip">>}]),
  788. {response, fin, 429, Headers} = gun:await(ConnPid, Ref),
  789. {_, <<"Fri, 22 Feb 2222 11:11:11 GMT">>} = lists:keyfind(<<"retry-after">>, 1, Headers),
  790. ok.
  791. rate_not_limited(Config) ->
  792. doc("A success response must be sent when the rate_limited callback returns false."),
  793. ConnPid = gun_open(Config),
  794. Ref = gun:get(ConnPid, "/rate_limited?false", [{<<"accept-encoding">>, <<"gzip">>}]),
  795. {response, nofin, 200, _} = gun:await(ConnPid, Ref),
  796. ok.
  797. stop_handler_allowed_methods(Config) ->
  798. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  799. stop_handler_allow_missing_post(Config) ->
  800. do_req_body_stop_handler(Config, post, ?FUNCTION_NAME).
  801. stop_handler_charsets_provided(Config) ->
  802. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  803. stop_handler_content_types_accepted(Config) ->
  804. do_req_body_stop_handler(Config, post, ?FUNCTION_NAME).
  805. stop_handler_content_types_provided(Config) ->
  806. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  807. stop_handler_delete_completed(Config) ->
  808. do_no_body_stop_handler(Config, delete, ?FUNCTION_NAME).
  809. stop_handler_delete_resource(Config) ->
  810. do_no_body_stop_handler(Config, delete, ?FUNCTION_NAME).
  811. stop_handler_forbidden(Config) ->
  812. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  813. stop_handler_is_authorized(Config) ->
  814. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  815. stop_handler_is_conflict(Config) ->
  816. do_req_body_stop_handler(Config, put, ?FUNCTION_NAME).
  817. stop_handler_known_methods(Config) ->
  818. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  819. stop_handler_languages_provided(Config) ->
  820. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  821. stop_handler_malformed_request(Config) ->
  822. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  823. stop_handler_moved_permanently(Config) ->
  824. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  825. stop_handler_moved_temporarily(Config) ->
  826. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  827. stop_handler_multiple_choices(Config) ->
  828. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  829. stop_handler_options(Config) ->
  830. do_no_body_stop_handler(Config, options, ?FUNCTION_NAME).
  831. stop_handler_previously_existed(Config) ->
  832. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  833. stop_handler_range_satisfiable(Config) ->
  834. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  835. stop_handler_ranges_provided(Config) ->
  836. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  837. stop_handler_rate_limited(Config) ->
  838. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  839. stop_handler_resource_exists(Config) ->
  840. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  841. stop_handler_service_available(Config) ->
  842. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  843. stop_handler_uri_too_long(Config) ->
  844. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  845. stop_handler_valid_content_headers(Config) ->
  846. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  847. stop_handler_valid_entity_length(Config) ->
  848. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  849. stop_handler_accept(Config) ->
  850. do_req_body_stop_handler(Config, post, ?FUNCTION_NAME).
  851. stop_handler_provide(Config) ->
  852. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  853. stop_handler_provide_range(Config) ->
  854. do_no_body_stop_handler(Config, get, ?FUNCTION_NAME).
  855. do_no_body_stop_handler(Config, Method, StateName0) ->
  856. doc("Send a response manually and stop the REST handler."),
  857. ConnPid = gun_open(Config),
  858. "stop_handler_" ++ StateName = atom_to_list(StateName0),
  859. Ref = gun:Method(ConnPid, "/stop_handler?" ++ StateName, [
  860. {<<"accept-encoding">>, <<"gzip">>},
  861. {<<"range">>, <<"bytes=0-">>}
  862. ]),
  863. {response, fin, 248, _} = gun:await(ConnPid, Ref),
  864. ok.
  865. do_req_body_stop_handler(Config, Method, StateName0) ->
  866. doc("Send a response manually and stop the REST handler."),
  867. ConnPid = gun_open(Config),
  868. "stop_handler_" ++ StateName = atom_to_list(StateName0),
  869. Ref = gun:Method(ConnPid, "/stop_handler?" ++ StateName, [
  870. {<<"accept-encoding">>, <<"gzip">>},
  871. {<<"content-type">>, <<"text/plain">>}
  872. ], <<"Hocus PocuSwitch!">>),
  873. {response, fin, 248, _} = gun:await(ConnPid, Ref),
  874. ok.
  875. switch_handler_allowed_methods(Config) ->
  876. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  877. switch_handler_allow_missing_post(Config) ->
  878. do_req_body_switch_handler(Config, post, ?FUNCTION_NAME).
  879. switch_handler_charsets_provided(Config) ->
  880. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  881. switch_handler_content_types_accepted(Config) ->
  882. do_req_body_switch_handler(Config, post, ?FUNCTION_NAME).
  883. switch_handler_content_types_provided(Config) ->
  884. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  885. switch_handler_delete_completed(Config) ->
  886. do_no_body_switch_handler(Config, delete, ?FUNCTION_NAME).
  887. switch_handler_delete_resource(Config) ->
  888. do_no_body_switch_handler(Config, delete, ?FUNCTION_NAME).
  889. switch_handler_forbidden(Config) ->
  890. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  891. switch_handler_is_authorized(Config) ->
  892. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  893. switch_handler_is_conflict(Config) ->
  894. do_req_body_switch_handler(Config, put, ?FUNCTION_NAME).
  895. switch_handler_known_methods(Config) ->
  896. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  897. switch_handler_languages_provided(Config) ->
  898. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  899. switch_handler_malformed_request(Config) ->
  900. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  901. switch_handler_moved_permanently(Config) ->
  902. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  903. switch_handler_moved_temporarily(Config) ->
  904. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  905. switch_handler_multiple_choices(Config) ->
  906. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  907. switch_handler_options(Config) ->
  908. do_no_body_switch_handler(Config, options, ?FUNCTION_NAME).
  909. switch_handler_previously_existed(Config) ->
  910. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  911. switch_handler_range_satisfiable(Config) ->
  912. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  913. switch_handler_ranges_provided(Config) ->
  914. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  915. switch_handler_rate_limited(Config) ->
  916. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  917. switch_handler_resource_exists(Config) ->
  918. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  919. switch_handler_service_available(Config) ->
  920. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  921. switch_handler_uri_too_long(Config) ->
  922. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  923. switch_handler_valid_content_headers(Config) ->
  924. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  925. switch_handler_valid_entity_length(Config) ->
  926. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  927. switch_handler_accept(Config) ->
  928. do_req_body_switch_handler(Config, post, ?FUNCTION_NAME).
  929. switch_handler_provide(Config) ->
  930. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  931. switch_handler_provide_range(Config) ->
  932. do_no_body_switch_handler(Config, get, ?FUNCTION_NAME).
  933. do_no_body_switch_handler(Config, Method, StateName0) ->
  934. doc("Switch REST to loop handler for streaming the response body, "
  935. "with and without options."),
  936. "switch_handler_" ++ StateName = atom_to_list(StateName0),
  937. do_no_body_switch_handler1(Config, Method, "/switch_handler?" ++ StateName),
  938. do_no_body_switch_handler1(Config, Method, "/switch_handler_opts?" ++ StateName).
  939. do_no_body_switch_handler1(Config, Method, Path) ->
  940. ConnPid = gun_open(Config),
  941. Ref = gun:Method(ConnPid, Path, [
  942. {<<"accept-encoding">>, <<"gzip">>},
  943. {<<"range">>, <<"bytes=0-">>}
  944. ]),
  945. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  946. {ok, Body} = gun:await_body(ConnPid, Ref),
  947. <<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body),
  948. ok.
  949. do_req_body_switch_handler(Config, Method, StateName0) ->
  950. doc("Switch REST to loop handler for streaming the response body, "
  951. "with and without options."),
  952. "switch_handler_" ++ StateName = atom_to_list(StateName0),
  953. do_req_body_switch_handler1(Config, Method, "/switch_handler?" ++ StateName),
  954. do_req_body_switch_handler1(Config, Method, "/switch_handler_opts?" ++ StateName).
  955. do_req_body_switch_handler1(Config, Method, Path) ->
  956. ConnPid = gun_open(Config),
  957. Ref = gun:Method(ConnPid, Path, [
  958. {<<"accept-encoding">>, <<"gzip">>},
  959. {<<"content-type">>, <<"text/plain">>}
  960. ], <<"Hocus PocuSwitch!">>),
  961. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  962. {ok, Body} = gun:await_body(ConnPid, Ref),
  963. <<"Hello\nstreamed\nworld!\n">> = do_decode(Headers, Body),
  964. ok.