cowboy_http_protocol.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. %% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
  2. %% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
  3. %%
  4. %% Permission to use, copy, modify, and/or distribute this software for any
  5. %% purpose with or without fee is hereby granted, provided that the above
  6. %% copyright notice and this permission notice appear in all copies.
  7. %%
  8. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  9. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  10. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  11. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  12. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  13. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  14. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  15. %% @doc HTTP protocol handler.
  16. %%
  17. %% The available options are:
  18. %% <dl>
  19. %% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd>
  20. %% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
  21. %% Defaults to 5.</dd>
  22. %% <dt>timeout</dt><dd>Time in milliseconds before an idle
  23. %% connection is closed. Defaults to 5000 milliseconds.</dd>
  24. %% <dt>urldecode</dt><dd>Function and options argument to use when decoding
  25. %% URL encoded strings. Defaults to `{fun cowboy_http:urldecode/2, crash}'.
  26. %% </dd>
  27. %% </dl>
  28. %%
  29. %% Note that there is no need to monitor these processes when using Cowboy as
  30. %% an application as it already supervises them under the listener supervisor.
  31. %%
  32. %% @see cowboy_dispatcher
  33. %% @see cowboy_http_handler
  34. -module(cowboy_http_protocol).
  35. -behaviour(cowboy_protocol).
  36. -export([start_link/4]). %% API.
  37. -export([init/4, parse_request/1, handler_loop/3]). %% FSM.
  38. -include("include/http.hrl").
  39. -include_lib("eunit/include/eunit.hrl").
  40. -record(state, {
  41. listener :: pid(),
  42. socket :: inet:socket(),
  43. transport :: module(),
  44. dispatch :: cowboy_dispatcher:dispatch_rules(),
  45. handler :: {module(), any()},
  46. urldecode :: {fun((binary(), T) -> binary()), T},
  47. req_empty_lines = 0 :: integer(),
  48. max_empty_lines :: integer(),
  49. req_keepalive = 1 :: integer(),
  50. max_keepalive :: integer(),
  51. max_line_length :: integer(),
  52. timeout :: timeout(),
  53. buffer = <<>> :: binary(),
  54. hibernate = false :: boolean(),
  55. loop_timeout = infinity :: timeout(),
  56. loop_timeout_ref :: undefined | reference()
  57. }).
  58. %% API.
  59. %% @doc Start an HTTP protocol process.
  60. -spec start_link(pid(), inet:socket(), module(), any()) -> {ok, pid()}.
  61. start_link(ListenerPid, Socket, Transport, Opts) ->
  62. Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, Opts]),
  63. {ok, Pid}.
  64. %% FSM.
  65. %% @private
  66. -spec init(pid(), inet:socket(), module(), any()) -> ok | none().
  67. init(ListenerPid, Socket, Transport, Opts) ->
  68. Dispatch = proplists:get_value(dispatch, Opts, []),
  69. MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
  70. MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity),
  71. MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
  72. Timeout = proplists:get_value(timeout, Opts, 5000),
  73. URLDecDefault = {fun cowboy_http:urldecode/2, crash},
  74. URLDec = proplists:get_value(urldecode, Opts, URLDecDefault),
  75. ok = cowboy:accept_ack(ListenerPid),
  76. wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
  77. dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
  78. max_keepalive=MaxKeepalive, max_line_length=MaxLineLength,
  79. timeout=Timeout, urldecode=URLDec}).
  80. %% @private
  81. -spec parse_request(#state{}) -> ok | none().
  82. %% We limit the length of the Request-line to MaxLength to avoid endlessly
  83. %% reading from the socket and eventually crashing.
  84. parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
  85. case erlang:decode_packet(http_bin, Buffer, []) of
  86. {ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
  87. {more, _Length} when byte_size(Buffer) > MaxLength ->
  88. error_terminate(413, State);
  89. {more, _Length} -> wait_request(State);
  90. {error, _Reason} -> error_terminate(400, State)
  91. end.
  92. -spec wait_request(#state{}) -> ok | none().
  93. wait_request(State=#state{socket=Socket, transport=Transport,
  94. timeout=T, buffer=Buffer}) ->
  95. case Transport:recv(Socket, 0, T) of
  96. {ok, Data} -> parse_request(State#state{
  97. buffer= << Buffer/binary, Data/binary >>});
  98. {error, _Reason} -> terminate(State)
  99. end.
  100. -spec request({http_request, http_method(), http_uri(),
  101. http_version()}, #state{}) -> ok | none().
  102. request({http_request, _Method, _URI, Version}, State)
  103. when Version =/= {1, 0}, Version =/= {1, 1} ->
  104. error_terminate(505, State);
  105. request({http_request, Method, {abs_path, AbsPath}, Version},
  106. State=#state{socket=Socket, transport=Transport,
  107. urldecode={URLDecFun, URLDecArg}=URLDec}) ->
  108. URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
  109. {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode),
  110. ConnAtom = version_to_connection(Version),
  111. parse_header(#http_req{socket=Socket, transport=Transport,
  112. connection=ConnAtom, method=Method, version=Version,
  113. path=Path, raw_path=RawPath, raw_qs=Qs, urldecode=URLDec}, State);
  114. request({http_request, Method, '*', Version},
  115. State=#state{socket=Socket, transport=Transport, urldecode=URLDec}) ->
  116. ConnAtom = version_to_connection(Version),
  117. parse_header(#http_req{socket=Socket, transport=Transport,
  118. connection=ConnAtom, method=Method, version=Version,
  119. path='*', raw_path= <<"*">>, raw_qs= <<>>, urldecode=URLDec}, State);
  120. request({http_request, _Method, _URI, _Version}, State) ->
  121. error_terminate(501, State);
  122. request({http_error, <<"\r\n">>},
  123. State=#state{req_empty_lines=N, max_empty_lines=N}) ->
  124. error_terminate(400, State);
  125. request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
  126. parse_request(State#state{req_empty_lines=N + 1});
  127. request(_Any, State) ->
  128. error_terminate(400, State).
  129. -spec parse_header(#http_req{}, #state{}) -> ok | none().
  130. parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
  131. case erlang:decode_packet(httph_bin, Buffer, []) of
  132. {ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
  133. {more, _Length} when byte_size(Buffer) > MaxLength ->
  134. error_terminate(413, State);
  135. {more, _Length} -> wait_header(Req, State);
  136. {error, _Reason} -> error_terminate(400, State)
  137. end.
  138. -spec wait_header(#http_req{}, #state{}) -> ok | none().
  139. wait_header(Req, State=#state{socket=Socket,
  140. transport=Transport, timeout=T, buffer=Buffer}) ->
  141. case Transport:recv(Socket, 0, T) of
  142. {ok, Data} -> parse_header(Req, State#state{
  143. buffer= << Buffer/binary, Data/binary >>});
  144. {error, timeout} -> error_terminate(408, State);
  145. {error, closed} -> terminate(State)
  146. end.
  147. -spec header({http_header, integer(), http_header(), any(), binary()}
  148. | http_eoh, #http_req{}, #state{}) -> ok | none().
  149. header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
  150. transport=Transport, host=undefined}, State) ->
  151. RawHost2 = cowboy_bstr:to_lower(RawHost),
  152. case catch cowboy_dispatcher:split_host(RawHost2) of
  153. {Host, RawHost3, undefined} ->
  154. Port = default_port(Transport:name()),
  155. dispatch(fun parse_header/2, Req#http_req{
  156. host=Host, raw_host=RawHost3, port=Port,
  157. headers=[{'Host', RawHost3}|Req#http_req.headers]}, State);
  158. {Host, RawHost3, Port} ->
  159. dispatch(fun parse_header/2, Req#http_req{
  160. host=Host, raw_host=RawHost3, port=Port,
  161. headers=[{'Host', RawHost3}|Req#http_req.headers]}, State);
  162. {'EXIT', _Reason} ->
  163. error_terminate(400, State)
  164. end;
  165. %% Ignore Host headers if we already have it.
  166. header({http_header, _I, 'Host', _R, _V}, Req, State) ->
  167. parse_header(Req, State);
  168. header({http_header, _I, 'Connection', _R, Connection},
  169. Req=#http_req{headers=Headers}, State) ->
  170. Req2 = Req#http_req{headers=[{'Connection', Connection}|Headers]},
  171. {ConnTokens, Req3}
  172. = cowboy_http_req:parse_header('Connection', Req2),
  173. ConnAtom = cowboy_http:connection_to_atom(ConnTokens),
  174. parse_header(Req3#http_req{connection=ConnAtom}, State);
  175. header({http_header, _I, Field, _R, Value}, Req, State) ->
  176. Field2 = format_header(Field),
  177. parse_header(Req#http_req{headers=[{Field2, Value}|Req#http_req.headers]},
  178. State);
  179. %% The Host header is required in HTTP/1.1.
  180. header(http_eoh, #http_req{version={1, 1}, host=undefined}, State) ->
  181. error_terminate(400, State);
  182. %% It is however optional in HTTP/1.0.
  183. header(http_eoh, Req=#http_req{version={1, 0}, transport=Transport,
  184. host=undefined}, State=#state{buffer=Buffer}) ->
  185. Port = default_port(Transport:name()),
  186. dispatch(fun handler_init/2, Req#http_req{host=[], raw_host= <<>>,
  187. port=Port, buffer=Buffer}, State#state{buffer= <<>>});
  188. header(http_eoh, Req, State=#state{buffer=Buffer}) ->
  189. handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
  190. header(_Any, _Req, State) ->
  191. error_terminate(400, State).
  192. -spec dispatch(fun((#http_req{}, #state{}) -> ok),
  193. #http_req{}, #state{}) -> ok | none().
  194. dispatch(Next, Req=#http_req{host=Host, path=Path},
  195. State=#state{dispatch=Dispatch}) ->
  196. %% @todo We should allow a configurable chain of handlers here to
  197. %% allow things like url rewriting, site-wide authentication,
  198. %% optional dispatching, and more. It would default to what
  199. %% we are doing so far.
  200. case cowboy_dispatcher:match(Host, Path, Dispatch) of
  201. {ok, Handler, Opts, Binds, HostInfo, PathInfo} ->
  202. Next(Req#http_req{host_info=HostInfo, path_info=PathInfo,
  203. bindings=Binds}, State#state{handler={Handler, Opts}});
  204. {error, notfound, host} ->
  205. error_terminate(400, State);
  206. {error, notfound, path} ->
  207. error_terminate(404, State)
  208. end.
  209. -spec handler_init(#http_req{}, #state{}) -> ok | none().
  210. handler_init(Req, State=#state{transport=Transport,
  211. handler={Handler, Opts}}) ->
  212. try Handler:init({Transport:name(), http}, Req, Opts) of
  213. {ok, Req2, HandlerState} ->
  214. handler_handle(HandlerState, Req2, State);
  215. {loop, Req2, HandlerState} ->
  216. handler_before_loop(HandlerState, Req2, State);
  217. {loop, Req2, HandlerState, hibernate} ->
  218. handler_before_loop(HandlerState, Req2,
  219. State#state{hibernate=true});
  220. {loop, Req2, HandlerState, Timeout} ->
  221. handler_before_loop(HandlerState, Req2,
  222. State#state{loop_timeout=Timeout});
  223. {loop, Req2, HandlerState, Timeout, hibernate} ->
  224. handler_before_loop(HandlerState, Req2,
  225. State#state{hibernate=true, loop_timeout=Timeout});
  226. {shutdown, Req2, HandlerState} ->
  227. handler_terminate(HandlerState, Req2, State);
  228. %% @todo {upgrade, transport, Module}
  229. {upgrade, protocol, Module} ->
  230. upgrade_protocol(Req, State, Module)
  231. catch Class:Reason ->
  232. error_terminate(500, State),
  233. error_logger:error_msg(
  234. "** Handler ~p terminating in init/3~n"
  235. " for the reason ~p:~p~n"
  236. "** Options were ~p~n"
  237. "** Request was ~p~n** Stacktrace: ~p~n~n",
  238. [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
  239. end.
  240. -spec upgrade_protocol(#http_req{}, #state{}, atom()) -> ok | none().
  241. upgrade_protocol(Req, State=#state{listener=ListenerPid,
  242. handler={Handler, Opts}}, Module) ->
  243. case Module:upgrade(ListenerPid, Handler, Opts, Req) of
  244. {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
  245. _Any -> terminate(State)
  246. end.
  247. -spec handler_handle(any(), #http_req{}, #state{}) -> ok | none().
  248. handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
  249. try Handler:handle(Req, HandlerState) of
  250. {ok, Req2, HandlerState2} ->
  251. terminate_request(HandlerState2, Req2, State)
  252. catch Class:Reason ->
  253. error_logger:error_msg(
  254. "** Handler ~p terminating in handle/2~n"
  255. " for the reason ~p:~p~n"
  256. "** Options were ~p~n** Handler state was ~p~n"
  257. "** Request was ~p~n** Stacktrace: ~p~n~n",
  258. [Handler, Class, Reason, Opts,
  259. HandlerState, Req, erlang:get_stacktrace()]),
  260. handler_terminate(HandlerState, Req, State),
  261. terminate(State)
  262. end.
  263. %% We don't listen for Transport closes because that would force us
  264. %% to receive data and buffer it indefinitely.
  265. -spec handler_before_loop(any(), #http_req{}, #state{}) -> ok | none().
  266. handler_before_loop(HandlerState, Req, State=#state{hibernate=true}) ->
  267. State2 = handler_loop_timeout(State),
  268. erlang:hibernate(?MODULE, handler_loop,
  269. [HandlerState, Req, State2#state{hibernate=false}]);
  270. handler_before_loop(HandlerState, Req, State) ->
  271. State2 = handler_loop_timeout(State),
  272. handler_loop(HandlerState, Req, State2).
  273. %% Almost the same code can be found in cowboy_http_websocket.
  274. -spec handler_loop_timeout(#state{}) -> #state{}.
  275. handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
  276. State#state{loop_timeout_ref=undefined};
  277. handler_loop_timeout(State=#state{loop_timeout=Timeout,
  278. loop_timeout_ref=PrevRef}) ->
  279. _ = case PrevRef of undefined -> ignore; PrevRef ->
  280. erlang:cancel_timer(PrevRef) end,
  281. TRef = make_ref(),
  282. erlang:send_after(Timeout, self(), {?MODULE, timeout, TRef}),
  283. State#state{loop_timeout_ref=TRef}.
  284. -spec handler_loop(any(), #http_req{}, #state{}) -> ok | none().
  285. handler_loop(HandlerState, Req, State=#state{loop_timeout_ref=TRef}) ->
  286. receive
  287. {?MODULE, timeout, TRef} ->
  288. terminate_request(HandlerState, Req, State);
  289. {?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) ->
  290. handler_loop(HandlerState, Req, State);
  291. Message ->
  292. handler_call(HandlerState, Req, State, Message)
  293. end.
  294. -spec handler_call(any(), #http_req{}, #state{}, any()) -> ok | none().
  295. handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
  296. Message) ->
  297. try Handler:info(Message, Req, HandlerState) of
  298. {ok, Req2, HandlerState2} ->
  299. terminate_request(HandlerState2, Req2, State);
  300. {loop, Req2, HandlerState2} ->
  301. handler_before_loop(HandlerState2, Req2, State);
  302. {loop, Req2, HandlerState2, hibernate} ->
  303. handler_before_loop(HandlerState2, Req2,
  304. State#state{hibernate=true})
  305. catch Class:Reason ->
  306. error_logger:error_msg(
  307. "** Handler ~p terminating in info/3~n"
  308. " for the reason ~p:~p~n"
  309. "** Options were ~p~n** Handler state was ~p~n"
  310. "** Request was ~p~n** Stacktrace: ~p~n~n",
  311. [Handler, Class, Reason, Opts,
  312. HandlerState, Req, erlang:get_stacktrace()])
  313. end.
  314. -spec handler_terminate(any(), #http_req{}, #state{}) -> ok.
  315. handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) ->
  316. try
  317. Handler:terminate(Req#http_req{resp_state=locked}, HandlerState)
  318. catch Class:Reason ->
  319. error_logger:error_msg(
  320. "** Handler ~p terminating in terminate/2~n"
  321. " for the reason ~p:~p~n"
  322. "** Options were ~p~n** Handler state was ~p~n"
  323. "** Request was ~p~n** Stacktrace: ~p~n~n",
  324. [Handler, Class, Reason, Opts,
  325. HandlerState, Req, erlang:get_stacktrace()])
  326. end.
  327. -spec terminate_request(any(), #http_req{}, #state{}) -> ok | none().
  328. terminate_request(HandlerState, Req, State) ->
  329. HandlerRes = handler_terminate(HandlerState, Req, State),
  330. next_request(Req, State, HandlerRes).
  331. -spec next_request(#http_req{}, #state{}, any()) -> ok | none().
  332. next_request(Req=#http_req{connection=Conn, buffer=Buffer},
  333. State=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive},
  334. HandlerRes) ->
  335. BodyRes = ensure_body_processed(Req),
  336. RespRes = ensure_response(Req),
  337. case {HandlerRes, BodyRes, RespRes, Conn} of
  338. {ok, ok, ok, keepalive} when Keepalive < MaxKeepalive ->
  339. ?MODULE:parse_request(State#state{
  340. buffer=Buffer, req_empty_lines=0,
  341. req_keepalive=Keepalive + 1});
  342. _Closed ->
  343. terminate(State)
  344. end.
  345. -spec ensure_body_processed(#http_req{}) -> ok | close.
  346. ensure_body_processed(#http_req{body_state=done}) ->
  347. ok;
  348. ensure_body_processed(Req=#http_req{body_state=waiting}) ->
  349. case cowboy_http_req:body(Req) of
  350. {error, badarg} -> ok; %% No body.
  351. {error, _Reason} -> close;
  352. _Any -> ok
  353. end.
  354. -spec ensure_response(#http_req{}) -> ok.
  355. %% The handler has already fully replied to the client.
  356. ensure_response(#http_req{resp_state=done}) ->
  357. ok;
  358. %% No response has been sent but everything apparently went fine.
  359. %% Reply with 204 No Content to indicate this.
  360. ensure_response(Req=#http_req{resp_state=waiting}) ->
  361. _ = cowboy_http_req:reply(204, [], [], Req),
  362. ok;
  363. %% Close the chunked reply.
  364. ensure_response(#http_req{method='HEAD', resp_state=chunks}) ->
  365. close;
  366. ensure_response(#http_req{socket=Socket, transport=Transport,
  367. resp_state=chunks}) ->
  368. Transport:send(Socket, <<"0\r\n\r\n">>),
  369. close.
  370. -spec error_terminate(http_status(), #state{}) -> ok.
  371. error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
  372. _ = cowboy_http_req:reply(Code, [], [], #http_req{
  373. socket=Socket, transport=Transport,
  374. connection=close, resp_state=waiting}),
  375. terminate(State).
  376. -spec terminate(#state{}) -> ok.
  377. terminate(#state{socket=Socket, transport=Transport}) ->
  378. Transport:close(Socket),
  379. ok.
  380. %% Internal.
  381. -spec version_to_connection(http_version()) -> keepalive | close.
  382. version_to_connection({1, 1}) -> keepalive;
  383. version_to_connection(_Any) -> close.
  384. -spec default_port(atom()) -> 80 | 443.
  385. default_port(ssl) -> 443;
  386. default_port(_) -> 80.
  387. %% @todo While 32 should be enough for everybody, we should probably make
  388. %% this configurable or something.
  389. -spec format_header(atom()) -> atom(); (binary()) -> binary().
  390. format_header(Field) when is_atom(Field) ->
  391. Field;
  392. format_header(Field) when byte_size(Field) =< 20; byte_size(Field) > 32 ->
  393. Field;
  394. format_header(Field) ->
  395. format_header(Field, true, <<>>).
  396. -spec format_header(binary(), boolean(), binary()) -> binary().
  397. format_header(<<>>, _Any, Acc) ->
  398. Acc;
  399. %% Replicate a bug in OTP for compatibility reasons when there's a - right
  400. %% after another. Proper use should always be 'true' instead of 'not Bool'.
  401. format_header(<< $-, Rest/bits >>, Bool, Acc) ->
  402. format_header(Rest, not Bool, << Acc/binary, $- >>);
  403. format_header(<< C, Rest/bits >>, true, Acc) ->
  404. format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_upper(C)) >>);
  405. format_header(<< C, Rest/bits >>, false, Acc) ->
  406. format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_lower(C)) >>).
  407. %% Tests.
  408. -ifdef(TEST).
  409. format_header_test_() ->
  410. %% {Header, Result}
  411. Tests = [
  412. {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>},
  413. {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>},
  414. {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>},
  415. {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>},
  416. %% These last tests ensures we're formatting headers exactly like OTP.
  417. %% Even though it's dumb, it's better for compatibility reasons.
  418. {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--version">>},
  419. {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>}
  420. ],
  421. [{H, fun() -> R = format_header(H) end} || {H, R} <- Tests].
  422. -endif.