aboutsummaryrefslogtreecommitdiffstats
path: root/test/req_SUITE.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2023-01-31 11:07:31 +0100
committerLoïc Hoguin <[email protected]>2024-03-26 15:53:48 +0100
commit8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9 (patch)
treeae2c323c3825da367e54704ea0b9ad80096059c3 /test/req_SUITE.erl
parent3ea8395eb8f53a57acb5d3c00b99c70296e7cdbd (diff)
downloadcowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.tar.gz
cowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.tar.bz2
cowboy-8cb9d242b0a665cada6de8b9a9dfa329e0c06ee9.zip
Initial HTTP/3 implementationhttp3
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.
Diffstat (limited to 'test/req_SUITE.erl')
-rw-r--r--test/req_SUITE.erl200
1 files changed, 138 insertions, 62 deletions
diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl
index 9f24ed1..9036cac 100644
--- a/test/req_SUITE.erl
+++ b/test/req_SUITE.erl
@@ -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) ->