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
Copy Markdown

@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
Copy Markdown

leoliu commented Feb 19, 2026

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

@benoitc
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
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
Copy Markdown
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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

erlang_quic seems to have a stream_opened message so perhaps this is dead code?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not dead code because evidently the message isn't sent, there's only leftovers here and there in erlang_quic.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The lack of a stream_opened event means we can receive a stream_reset or a stop_sending without having received any data first, which is awkward.

Comment thread src/cowboy_http3.erl
{webtransport_session, _}}, <<>>, fin) ->
webtransport_event(State, SessionID, {closed, 0, <<>>}),
?QUIC_ADAPTER:shutdown_stream(Conn, SessionID),
loop(webtransport_terminate_session(State, Stream));
Copy link
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
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
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
%% 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
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
%% 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
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
%% 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
Copy Markdown
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).

Comment thread src/cowboy_http3.erl
%% 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
Copy Markdown
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.

Comment thread src/cowboy_http3.erl
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
Copy Markdown
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
Copy Markdown
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.

@essen
Copy link
Copy Markdown
Member

essen commented Mar 12, 2026

I should have the initial work on the quic/quicer abstraction available some time next week. Putting on the finishing touches now.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants