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:
parent
3ea8395eb8
commit
8cb9d242b0
39 changed files with 5130 additions and 238 deletions
|
@ -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) ->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue