0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Initial HTTP/3 implementation

This includes Websocket over HTTP/3.

Since quicer, which provides the QUIC implementation,
is a NIF, Cowboy cannot depend directly on it. In order
to enable QUIC and HTTP/3, users have to set the
COWBOY_QUICER environment variable:

  export COWBOY_QUICER=1

In order to run the test suites, the same must be done
for Gun:

  export GUN_QUICER=1

HTTP/3 support is currently not available on Windows
due to compilation issues of quicer which have yet to
be looked at or resolved.

HTTP/3 support is also unavailable on the upcoming
OTP-27 due to compilation errors in quicer dependencies.
Once resolved HTTP/3 should work on OTP-27.

Because of how QUIC currently works, it's possible
that streams that get reset after sending a response
do not receive that response. The test suite was
modified to accomodate for that. A future extension
to QUIC will allow us to gracefully reset streams.

This also updates Erlang.mk.
This commit is contained in:
Loïc Hoguin 2023-01-31 11:07:31 +01:00
parent 3ea8395eb8
commit 8cb9d242b0
No known key found for this signature in database
GPG key ID: 8A9DF795F6FED764
39 changed files with 5130 additions and 238 deletions

View file

@ -46,7 +46,7 @@ init_per_group(Name, Config) ->
cowboy_test:init_common_groups(Name, Config, ?MODULE).
end_per_group(Name, _) ->
cowboy:stop_listener(Name).
cowboy_test:stop_group(Name).
%% Routes.
@ -107,13 +107,17 @@ do_get(Path, Config) ->
do_get(Path, Headers, Config) ->
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, Path, [{<<"accept-encoding">>, <<"gzip">>}|Headers]),
{response, IsFin, Status, RespHeaders} = gun:await(ConnPid, Ref, infinity),
{ok, RespBody} = case IsFin of
nofin -> gun:await_body(ConnPid, Ref, infinity);
fin -> {ok, <<>>}
end,
gun:close(ConnPid),
{Status, RespHeaders, do_decode(RespHeaders, RespBody)}.
case gun:await(ConnPid, Ref, infinity) of
{response, IsFin, Status, RespHeaders} ->
{ok, RespBody} = case IsFin of
nofin -> gun:await_body(ConnPid, Ref, infinity);
fin -> {ok, <<>>}
end,
gun:close(ConnPid),
{Status, RespHeaders, do_decode(RespHeaders, RespBody)};
{error, {stream_error, Error}} ->
Error
end.
do_get_body(Path, Config) ->
do_get_body(Path, [], Config).
@ -142,7 +146,9 @@ do_get_inform(Path, Config) ->
fin -> {ok, <<>>}
end,
gun:close(ConnPid),
{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)}
{InfoStatus, InfoHeaders, RespStatus, RespHeaders, do_decode(RespHeaders, RespBody)};
{error, {stream_error, Error}} ->
Error
end.
do_decode(Headers, Body) ->
@ -184,7 +190,8 @@ bindings(Config) ->
cert(Config) ->
case config(type, Config) of
tcp -> doc("TLS certificates can only be provided over TLS.");
ssl -> do_cert(Config)
ssl -> do_cert(Config);
quic -> do_cert(Config)
end.
do_cert(Config) ->
@ -386,7 +393,8 @@ port(Config) ->
Port = do_get_body("/direct/port", Config),
ExpectedPort = case config(type, Config) of
tcp -> <<"80">>;
ssl -> <<"443">>
ssl -> <<"443">>;
quic -> <<"443">>
end,
ExpectedPort = do_get_body("/port", [{<<"host">>, <<"localhost">>}], Config),
ExpectedPort = do_get_body("/direct/port", [{<<"host">>, <<"localhost">>}], Config),
@ -412,7 +420,8 @@ do_scheme(Path, Config) ->
Transport = config(type, Config),
case do_get_body(Path, Config) of
<<"http">> when Transport =:= tcp -> ok;
<<"https">> when Transport =:= ssl -> ok
<<"https">> when Transport =:= ssl -> ok;
<<"https">> when Transport =:= quic -> ok
end.
sock(Config) ->
@ -425,7 +434,8 @@ uri(Config) ->
doc("Request URI building/modification."),
Scheme = case config(type, Config) of
tcp -> <<"http">>;
ssl -> <<"https">>
ssl -> <<"https">>;
quic -> <<"https">>
end,
SLen = byte_size(Scheme),
Port = integer_to_binary(config(port, Config)),
@ -459,7 +469,8 @@ do_version(Path, Config) ->
Protocol = config(protocol, Config),
case do_get_body(Path, Config) of
<<"HTTP/1.1">> when Protocol =:= http -> ok;
<<"HTTP/2">> when Protocol =:= http2 -> ok
<<"HTTP/2">> when Protocol =:= http2 -> ok;
<<"HTTP/3">> when Protocol =:= http3 -> ok
end.
%% Tests: Request body.
@ -513,11 +524,19 @@ read_body_period(Config) ->
%% for 2 seconds. The test succeeds if we get some of the data back
%% (meaning the function will have returned after the period ends).
gun:data(ConnPid, Ref, nofin, Body),
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
{data, _, Data} = gun:await(ConnPid, Ref, infinity),
%% We expect to read at least some data.
true = Data =/= <<>>,
gun:close(ConnPid).
Response = gun:await(ConnPid, Ref, infinity),
case Response of
{response, nofin, 200, _} ->
{data, _, Data} = gun:await(ConnPid, Ref, infinity),
%% We expect to read at least some data.
true = Data =/= <<>>,
gun:close(ConnPid);
%% We got a crash, likely because the environment
%% was overloaded and the timeout triggered. Try again.
{response, _, 500, _} ->
gun:close(ConnPid),
read_body_period(Config)
end.
%% We expect a crash.
do_read_body_timeout(Path, Body, Config) ->
@ -525,7 +544,13 @@ do_read_body_timeout(Path, Body, Config) ->
Ref = gun:headers(ConnPid, "POST", Path, [
{<<"content-length">>, integer_to_binary(byte_size(Body))}
]),
{response, _, 500, _} = gun:await(ConnPid, Ref, infinity),
case gun:await(ConnPid, Ref, infinity) of
{response, _, 500, _} ->
ok;
%% See do_maybe_h3_error comment for details.
{error, {stream_error, {stream_error, h3_internal_error, _}}} ->
ok
end,
gun:close(ConnPid).
read_body_auto(Config) ->
@ -620,15 +645,19 @@ do_read_urlencoded_body_too_long(Path, Body, Config) ->
{<<"content-length">>, integer_to_binary(byte_size(Body) * 2)}
]),
gun:data(ConnPid, Ref, nofin, Body),
{response, _, 408, RespHeaders} = gun:await(ConnPid, Ref, infinity),
_ = case config(protocol, Config) of
http ->
Protocol = config(protocol, Config),
case gun:await(ConnPid, Ref, infinity) of
{response, _, 408, RespHeaders} when Protocol =:= http ->
%% 408 error responses should close HTTP/1.1 connections.
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders);
http2 ->
ok
end,
gun:close(ConnPid).
{_, <<"close">>} = lists:keyfind(<<"connection">>, 1, RespHeaders),
gun:close(ConnPid);
{response, _, 408, _} when Protocol =:= http2; Protocol =:= http3 ->
gun:close(ConnPid);
%% We must have hit the timeout due to busy CI environment. Retry.
{response, _, 500, _} ->
gun:close(ConnPid),
do_read_urlencoded_body_too_long(Path, Body, Config)
end.
read_and_match_urlencoded_body(Config) ->
doc("Read and match an application/x-www-form-urlencoded request body."),
@ -824,7 +853,7 @@ set_resp_header(Config) ->
{200, Headers, <<"OK">>} = do_get("/resp/set_resp_header", Config),
true = lists:keymember(<<"content-type">>, 1, Headers),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/set_resp_header_cookie", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_header_cookie", Config)),
ok.
set_resp_headers(Config) ->
@ -833,7 +862,7 @@ set_resp_headers(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers),
true = lists:keymember(<<"content-encoding">>, 1, Headers),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/set_resp_headers_cookie", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/set_resp_headers_cookie", Config)),
ok.
resp_header(Config) ->
@ -895,28 +924,52 @@ delete_resp_header(Config) ->
false = lists:keymember(<<"content-type">>, 1, Headers),
ok.
%% Data may be lost due to how RESET_STREAM QUIC frame works.
%% Because there is ongoing work for a better way to reset streams
%% (https://www.ietf.org/archive/id/draft-ietf-quic-reliable-stream-reset-03.html)
%% we convert the error to a 500 to keep the tests more explicit
%% at what we expect.
%% @todo When RESET_STREAM_AT gets added we can remove this function.
do_maybe_h3_error2({stream_error, h3_internal_error, _}) -> {500, []};
do_maybe_h3_error2(Result) -> Result.
do_maybe_h3_error3({stream_error, h3_internal_error, _}) -> {500, [], <<>>};
do_maybe_h3_error3(Result) -> Result.
inform2(Config) ->
doc("Informational response(s) without headers, followed by the real response."),
{102, [], 200, _, _} = do_get_inform("/resp/inform2/102", Config),
{102, [], 200, _, _} = do_get_inform("/resp/inform2/binary", Config),
{500, _} = do_get_inform("/resp/inform2/error", Config),
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform2/error", Config)),
{102, [], 200, _, _} = do_get_inform("/resp/inform2/twice", Config),
%% @todo How to test this properly? This isn't enough.
{200, _} = do_get_inform("/resp/inform2/after_reply", Config),
ok.
%% With HTTP/1.1 and HTTP/2 we will not get an error.
%% With HTTP/3 however the stream will occasionally
%% be reset before Gun receives the response.
case do_get_inform("/resp/inform2/after_reply", Config) of
{200, _} ->
ok;
{stream_error, h3_internal_error, _} ->
ok
end.
inform3(Config) ->
doc("Informational response(s) with headers, followed by the real response."),
Headers = [{<<"ext-header">>, <<"ext-value">>}],
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/102", Config),
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/binary", Config),
{500, _} = do_get_inform("/resp/inform3/error", Config),
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _} = do_get_inform("/resp/inform3/set_cookie", Config),
{500, _} = do_maybe_h3_error2(do_get_inform("/resp/inform3/set_cookie", Config)),
{102, Headers, 200, _, _} = do_get_inform("/resp/inform3/twice", Config),
%% @todo How to test this properly? This isn't enough.
{200, _} = do_get_inform("/resp/inform3/after_reply", Config),
ok.
%% With HTTP/1.1 and HTTP/2 we will not get an error.
%% With HTTP/3 however the stream will occasionally
%% be reset before Gun receives the response.
case do_get_inform("/resp/inform3/after_reply", Config) of
{200, _} ->
ok;
{stream_error, h3_internal_error, _} ->
ok
end.
reply2(Config) ->
doc("Response with default headers and no body."),
@ -924,7 +977,7 @@ reply2(Config) ->
{201, _, _} = do_get("/resp/reply2/201", Config),
{404, _, _} = do_get("/resp/reply2/404", Config),
{200, _, _} = do_get("/resp/reply2/binary", Config),
{500, _, _} = do_get("/resp/reply2/error", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply2/error", Config)),
%% @todo How to test this properly? This isn't enough.
{200, _, _} = do_get("/resp/reply2/twice", Config),
ok.
@ -937,9 +990,9 @@ reply3(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers2),
{404, Headers3, _} = do_get("/resp/reply3/404", Config),
true = lists:keymember(<<"content-type">>, 1, Headers3),
{500, _, _} = do_get("/resp/reply3/error", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/reply3/set_cookie", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply3/set_cookie", Config)),
ok.
reply4(Config) ->
@ -947,9 +1000,9 @@ reply4(Config) ->
{200, _, <<"OK">>} = do_get("/resp/reply4/200", Config),
{201, _, <<"OK">>} = do_get("/resp/reply4/201", Config),
{404, _, <<"OK">>} = do_get("/resp/reply4/404", Config),
{500, _, _} = do_get("/resp/reply4/error", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/reply4/set_cookie", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/reply4/set_cookie", Config)),
ok.
stream_reply2(Config) ->
@ -959,12 +1012,11 @@ stream_reply2(Config) ->
{201, _, Body} = do_get("/resp/stream_reply2/201", Config),
{404, _, Body} = do_get("/resp/stream_reply2/404", Config),
{200, _, Body} = do_get("/resp/stream_reply2/binary", Config),
{500, _, _} = do_get("/resp/stream_reply2/error", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply2/error", Config)),
ok.
stream_reply2_twice(Config) ->
doc("Attempting to stream a response twice results in a crash. "
"This crash can only be properly detected in HTTP/2."),
doc("Attempting to stream a response twice results in a crash."),
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/resp/stream_reply2/twice",
[{<<"accept-encoding">>, <<"gzip">>}]),
@ -983,8 +1035,10 @@ stream_reply2_twice(Config) ->
zlib:inflateInit(Z, 31),
0 = iolist_size(zlib:inflate(Z, Data)),
ok;
%% In HTTP/2 the stream gets reset with an appropriate error.
%% In HTTP/2 and HTTP/3 the stream gets reset with an appropriate error.
{http2, _, {error, {stream_error, {stream_error, internal_error, _}}}} ->
ok;
{http3, _, {error, {stream_error, {stream_error, h3_internal_error, _}}}} ->
ok
end,
gun:close(ConnPid).
@ -998,9 +1052,9 @@ stream_reply3(Config) ->
true = lists:keymember(<<"content-type">>, 1, Headers2),
{404, Headers3, Body} = do_get("/resp/stream_reply3/404", Config),
true = lists:keymember(<<"content-type">>, 1, Headers3),
{500, _, _} = do_get("/resp/stream_reply3/error", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/error", Config)),
%% The set-cookie header is special. set_resp_cookie must be used.
{500, _, _} = do_get("/resp/stream_reply3/set_cookie", Config),
{500, _, _} = do_maybe_h3_error3(do_get("/resp/stream_reply3/set_cookie", Config)),
ok.
stream_body_fin0(Config) ->
@ -1084,8 +1138,11 @@ stream_body_content_length_nofin_error(Config) ->
end
end;
http2 ->
%% @todo HTTP2 should have the same content-length checks
ok
%% @todo HTTP/2 should have the same content-length checks.
{skip, "Implement the test for HTTP/2."};
http3 ->
%% @todo HTTP/3 should have the same content-length checks.
{skip, "Implement the test for HTTP/3."}
end.
stream_body_concurrent(Config) ->
@ -1187,16 +1244,24 @@ stream_trailers_set_cookie(Config) ->
{<<"accept-encoding">>, <<"gzip">>},
{<<"te">>, <<"trailers">>}
]),
{response, nofin, 200, _} = gun:await(ConnPid, Ref, infinity),
case config(protocol, Config) of
http ->
Protocol = config(protocol, Config),
case gun:await(ConnPid, Ref, infinity) of
{response, nofin, 200, _} when Protocol =:= http ->
%% Trailers are not sent because of the stream error.
{ok, _Body} = gun:await_body(ConnPid, Ref, infinity),
{error, timeout} = gun:await_body(ConnPid, Ref, 1000),
ok;
http2 ->
{response, nofin, 200, _} when Protocol =:= http2 ->
{error, {stream_error, {stream_error, internal_error, _}}}
= gun:await_body(ConnPid, Ref, infinity),
ok;
{response, nofin, 200, _} when Protocol =:= http3 ->
{error, {stream_error, {stream_error, h3_internal_error, _}}}
= gun:await_body(ConnPid, Ref, infinity),
ok;
%% The RST_STREAM arrived before the start of the response.
%% See maybe_h3_error comment for details.
{error, {stream_error, {stream_error, h3_internal_error, _}}} when Protocol =:= http3 ->
ok
end,
gun:close(ConnPid).
@ -1224,34 +1289,45 @@ do_trailers(Path, Config) ->
push(Config) ->
case config(protocol, Config) of
http -> do_push_http("/resp/push", Config);
http2 -> do_push_http2(Config)
http2 -> do_push_http2(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end.
push_after_reply(Config) ->
doc("Trying to push a response after the final response results in a crash."),
ConnPid = gun_open(Config),
Ref = gun:get(ConnPid, "/resp/push/after_reply", []),
%% @todo How to test this properly? This isn't enough.
{response, fin, 200, _} = gun:await(ConnPid, Ref, infinity),
%% With HTTP/1.1 and HTTP/2 we will not get an error.
%% With HTTP/3 however the stream will occasionally
%% be reset before Gun receives the response.
case gun:await(ConnPid, Ref, infinity) of
{response, fin, 200, _} ->
ok;
{error, {stream_error, {stream_error, h3_internal_error, _}}} ->
ok
end,
gun:close(ConnPid).
push_method(Config) ->
case config(protocol, Config) of
http -> do_push_http("/resp/push/method", Config);
http2 -> do_push_http2_method(Config)
http2 -> do_push_http2_method(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end.
push_origin(Config) ->
case config(protocol, Config) of
http -> do_push_http("/resp/push/origin", Config);
http2 -> do_push_http2_origin(Config)
http2 -> do_push_http2_origin(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end.
push_qs(Config) ->
case config(protocol, Config) of
http -> do_push_http("/resp/push/qs", Config);
http2 -> do_push_http2_qs(Config)
http2 -> do_push_http2_qs(Config);
http3 -> {skip, "Implement server push for HTTP/3."}
end.
do_push_http(Path, Config) ->