Skip to content

Add HTTP/3 support with pure Erlang QUIC#1707

Open
benoitc wants to merge 2 commits intoninenines:masterfrom
benoitc:master
Open

Add HTTP/3 support with pure Erlang QUIC#1707
benoitc wants to merge 2 commits intoninenines:masterfrom
benoitc:master

Conversation

@benoitc
Copy link

@benoitc benoitc commented Feb 19, 2026

Summary

  • Add HTTP/3 support using erlang_quic (pure Erlang, no NIF)
  • Fix crashes in WebTransport handling
  • Add CVE-2019-9514 protection (stream reset rate limiting)
  • Implement GOAWAY, CONNECT method, proper error handling

How to test

Build with QUIC enabled:

COWBOY_QUIC=1 make

Run all tests:

COWBOY_QUIC=1 make eunit
COWBOY_QUIC=1 make ct CT_SUITES=h3

Or use the test script:

./test_h3.sh

@leoliu
Copy link

leoliu commented Feb 19, 2026

Absolutely amazing! This will make HTTP3 much more accessible. Thanks.

@benoitc
Copy link
Author

benoitc commented Feb 20, 2026

I've updated quic to support remaining parts of the spec. All interop tests are passing.

@essen
Copy link
Member

essen commented Feb 20, 2026

Hello! Thanks for the PR, I will start looking at it next week. Note however that removing quicer is probably too early as we might ship Tanzu RabbitMQ with it this year.

@benoitc benoitc force-pushed the master branch 2 times, most recently from 520cbb0 to 53cd105 Compare February 21, 2026 10:25
@benoitc
Copy link
Author

benoitc commented Feb 21, 2026

hrm ok i can re-add it. then What do you prefer, having an enviironment variable? ANyway all tests pass now. ANd quic pass all validiation tests. Let me know

@essen
Copy link
Member

essen commented Feb 21, 2026

Yes there should be an environment variable. Your early solution of having both COWBOY_QUICER and COWBOY_QUIC was fine. The default should be no dependency on QUIC for now.

You removed all the HTTP/3, Websocket over HTTP/3 and WebTransport tests as well, they should be added back. You also removed the common h3 test groups so the PR runs very little HTTP/3 tests right now.

I will enable tests in the PR, just noticed they're waiting approval.

@benoitc
Copy link
Author

benoitc commented Feb 21, 2026

well for me quicer doesn't work at all, their main branch doesn't even compile. Tests are failing. Anyway I will re-add the tests. is RFC 9220 supported by browsers these days ?

@essen
Copy link
Member

essen commented Feb 21, 2026

quicer is broken against OTP master right now. It works fine with 28 on CI (https://github.com/ninenines/cowboy/actions/runs/22048680378/job/63702146145).

Yes HTTP/3 Websocket is available but only behind flags. It's possible it never gets enabled by default in browsers but browsers aren't the only use case for Websocket.

This adds HTTP/3 support to Cowboy using two QUIC backends:

- erlang_quic: Pure Erlang QUIC implementation (default)
- quicer: NIF-based wrapper around MsQuic (optional, set COWBOY_QUICER=1)

Features:
- RFC 9114 HTTP/3 support
- RFC 9220 WebSocket over HTTP/3
- WebTransport over HTTP/3 (draft-ietf-webtrans-http3)
- Unified adapter interface for both backends
- Add http3-erlang-quic job to test pure Erlang QUIC backend
- Add http3-quicer job to test quicer/MsQuic backend
- Configure git credentials for quicer dependency clone
- Mark quicer job as continue-on-error (adapter needs updates)
@benoitc
Copy link
Author

benoitc commented Feb 22, 2026

@essen i've cleaned the patch. I removed the ci changes preventing testing with quicer. This should be OK now. tests with quic passent.

@essen
Copy link
Member

essen commented Feb 24, 2026

Thanks. No action is required at this point on your part, what follows are my thoughts on how to proceed.

So far I have reviewed erlang_quic itself. I plan to work on making erlang_quic the default, although for now it will be disabled by default. I think the security especially around handshake/auth/verify remains to be proven but this is just a matter of erlang_quic becoming more mature / battletested. To be enabled by default it will also need to reach at least 1.0, and doing this will bump Cowboy to 3.0.

I expect the performance of erlang_quic to not be enough for certain high throughput scenarios. Having to make everything go through a single connection process is definitely a pain point. It is fine for many use cases. But for the few cases where high throughput is important I want to make quicer available as a drop-in replacement. My initial goal while reviewing the PR will therefore be how best to keep Cowboy compatible with both of them. This might result in a Ranch-like project specifically for QUIC that would also have Ranch-compatible listener information, where applicable, to make it easier to work with both QUIC and non-QUIC listeners at the same time. The listener start/stop and cowboy_quicer types of modules would fit there.

Finally, when erlang_quic gets enabled by default, or later, Cowboy probably should have functions to start both TCP and QUIC listeners at the same time with a shared configuration that would automatically set Alt-Svc and friends should the user want that.

I will now review the PR and putting notes for myself (again, no action required for now).

Copy link
Member

@essen essen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action required on your part.

Supporting both erlang_quic and quicer in a configurable manner should be easy enough. There are two sides: fetching as dep (no dep should be fetched by default initially) and choosing the backend (we should allow both at the same time in different listeners).

My plan is to create a new Ranch-like dependency that optionally depends on one or two of the QUIC implementations. The initial default in Cowboy will be to not depend on this Ranch-like dependency (similar to what it does with COWBOY_QUICER). The dependency will contain what I described in the previous comment.

The PR contains a lot of changes that are not directly related and they are not separate commit so I will keep an eye out on the other changes after adding support for the two QUIC implementations.

loop(State)
%% Stream not found. For erlang_quic, streams are created
%% lazily when data arrives (no stream_opened notification).
%% Determine stream type from ID and create the stream.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably best handled in the adapter (adapter could return two commands/events instead of one).

{webtransport_session, _}}, <<>>, fin) ->
webtransport_event(State, SessionID, {closed, 0, <<>>}),
?QUIC_ADAPTER:shutdown_stream(Conn, SessionID),
loop(webtransport_terminate_session(State, Stream));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes like this one don't have a test and are unrelated to adding support so will be left for later.

stream_store(State, Stream);
trailers_frame(State=#state{opts=Opts},
Stream=#stream{id=StreamID, state=StreamState0}, Trailers) ->
try cowboy_stream:info(StreamID, {trailers, headers_to_map(Trailers, #{})}, StreamState0) of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trailers info message is to send response trailers back. Request trailers are currently ignored in Cowboy. Anyway this is not relevant for erlang_quicer either.

%% Only :method and :authority pseudo-headers are allowed.
headers_frame(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
Stream=#stream{id=StreamID}, IsFin, Headers,
PseudoHeaders=#{method := <<"CONNECT">>, authority := Authority}, _)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, CONNECT is not currently supported and unrelated to the QUIC support, it would need to be added to all protocols at once.

%% This can happen for datagrams arriving for sessions that
%% have already been terminated, or for future sessions
%% that haven't been established yet.
loop(State)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't want to silence this clause yet.

%% HTTP/2 and HTTP/3 server push has been deprecated and removed by all major
%% browsers: Chrome removed support in v106 (2022), Firefox in v132 (Oct 2024).
%% The feature provided minimal real-world benefit and added complexity.
%% Therefore, push promises are intentionally not supported.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still part of the standard and applications may rely on it. Browsers aren't everything. So for now there's no need to decisively drop them (which needs to be done for all protocols at once) nor is there a need to implement them for HTTP/3 (code can stay commented out like it was).

%% The QUIC stack sends MAX_STREAM_DATA frames to the peer when it's ready to
%% receive more data. Therefore, the {flow, Size} command is a no-op for HTTP/3.
commands(State, Stream, [{flow, _Size}|Tail]) ->
%% @todo We should tell the QUIC stream to increase its window size.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The todo is still valid, we should be able to provide hints to the QUIC stack that we are about to receive larger data sizes. This has improved performance for TCP so there's chances this would for some QUIC implementations as well.

cowboy:log(warning, "Failed to send WT_DRAIN_SESSION: ~p", [Reason], Opts)
end,
wt_commands(State, Session, Tail);
wt_commands(State0=#state{conn=Conn, opts=Opts}, Session=#stream{id=SessionID}, [Cmd|Tail])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WT changes unrelated AFAICT.

post_with_body(Config) ->
doc("POST request with body that gets echoed back. (RFC9114 4.1)"),
Port = config(port, Config),
{ok, Conn} = quic:connect("localhost", Port,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as Cowboy is concerned it's probably fine to test everything (both erlang_quic and quicer) with just one or the other. Since quicer is better battle tested it's probably a better option initially.

There should be no problem depending on both as well as long as there are no module conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants