Skip to content

Commit 1753824

Browse files
committed
refactor(http3): delegate H3 stack to erlang_quic / quic_h3
Hackney's hand-rolled HTTP/3 framing, QPACK codec, control stream and QPACK encoder/decoder stream handling are removed in favour of the quic_h3 module shipped in erlang_quic's feat/http3 branch. - src/hackney_quic.erl rewritten as a ~270 LOC gen_server adapter that translates {quic_h3, Conn, _} events into the existing {quic, ConnRef, _} protocol consumed by hackney_conn. - Public hackney_quic API now exposes send_request/3 (atomic stream open + HEADERS) instead of the open_stream/1 + send_headers/4 pair; hackney_h3 updated accordingly. - src/hackney_qpack.erl removed (~622 LOC); QPACK lives in quic_qpack. - H3 peername/sockname/setopts/peercert now return {error, not_supported} since quic_h3 does not expose them. - rebar.config: quic dep tracks erlang_quic feat/http3 branch.
1 parent fdd1576 commit 1753824

11 files changed

Lines changed: 221 additions & 1834 deletions

NEWS.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# NEWS
22

3+
UNRELEASED
4+
----------
5+
6+
### Refactor
7+
8+
- HTTP/3 is now delegated to the `erlang_quic` library's `quic_h3` module
9+
(branch `feat/http3`). Hackney no longer ships its own HTTP/3 framing,
10+
QPACK codec, control-stream or unidirectional-stream handling:
11+
- `hackney_quic.erl` is now a thin ~270 LOC gen_server adapter that
12+
translates `{quic_h3, Conn, _}` events into the existing
13+
`{quic, ConnRef, _}' message protocol consumed by `hackney_conn`.
14+
- The public `hackney_quic` API now exposes `send_request/3` (atomic
15+
stream-open + HEADERS) instead of the separate `open_stream/1` +
16+
`send_headers/4` pair; `hackney_h3` updated to use it.
17+
- `hackney_qpack.erl` removed (~622 LOC); the QPACK codec lives in
18+
`quic_qpack` in the `quic` dependency.
19+
- H3 peername/sockname/setopts/peercert now return `{error, not_supported}`:
20+
the underlying `quic_h3` connection does not expose them.
21+
- `rebar.config`: `quic` dependency switched to
22+
`{git, "https://github.com/benoitc/erlang_quic.git", {branch, "feat/http3"}}`.
23+
324
3.2.1 - 2026-03-01
425
------------------
526

guides/http3_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Hackney supports HTTP/3, the latest version of HTTP built on QUIC (UDP-based tra
1717

1818
## Requirements
1919

20-
HTTP/3 support uses a pure Erlang QUIC implementation. QUIC support is available automatically when hackney is compiled - no external dependencies required.
20+
HTTP/3 support is provided by the [`erlang_quic`](https://github.com/benoitc/erlang_quic) dependency (module `quic_h3`), which handles the QUIC transport, QPACK header compression, HTTP/3 framing, and control streams. Hackney hosts only a thin adapter (`hackney_quic`) that translates `quic_h3` events into the internal connection protocol. No C dependencies, no external binaries required.
2121

2222
## Quick Start
2323

rebar.config

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,18 @@
2525
{prometheus_gauge, declare, 1},
2626
{prometheus_histogram, observe, 3},
2727
{prometheus_histogram, declare, 1},
28-
%% quic (HTTP/3 support)
29-
{quic, connect, 4},
30-
{quic, open_stream, 1},
31-
{quic, open_unidirectional_stream, 1},
32-
{quic, send_data, 4},
33-
{quic, reset_stream, 3},
34-
{quic, peername, 1},
35-
{quic, sockname, 1},
36-
{quic, close, 2},
37-
{quic_connection, lookup, 1},
38-
%% hackney_quic wrapper functions called by hackney_conn
28+
%% quic / quic_h3 (HTTP/3 support)
29+
{quic_h3, connect, 3},
30+
{quic_h3, request, 2},
31+
{quic_h3, send_data, 4},
32+
{quic_h3, cancel, 3},
33+
{quic_h3, close, 1},
34+
%% hackney_quic wrapper functions called by hackney_conn/hackney_h3
3935
{hackney_quic, connect, 4},
4036
{hackney_quic, close, 2},
4137
{hackney_quic, process, 1},
42-
{hackney_quic, peername, 1},
43-
{hackney_quic, sockname, 1},
44-
{hackney_quic, setopts, 2},
38+
{hackney_quic, send_request, 3},
39+
{hackney_quic, send_data, 4},
4540
%% hackney_h3 functions called by hackney_conn
4641
{hackney_h3, send_request, 6},
4742
{hackney_h3, send_request_headers, 5},
@@ -56,8 +51,9 @@
5651
{pre_hooks, [{clean, "rm -rf *~ */*~ */*.xfm test/*.beam"}]}.
5752

5853
{deps, [
59-
%% Pure Erlang QUIC for HTTP/3 support
60-
{quic, "~>0.10.2"},
54+
%% Pure Erlang QUIC + HTTP/3 stack
55+
{quic, {git, "https://github.com/benoitc/erlang_quic.git",
56+
{branch, "feat/http3"}}},
6157
{idna, "~>7.1.0"},
6258
{mimerl, "~>1.4"},
6359
{certifi, "~>2.16.0"},

src/hackney_conn.erl

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,26 +1509,23 @@ handle_common({call, From}, sockname, _State, #conn_data{transport = Transport,
15091509
Result = Transport:sockname(Socket),
15101510
{keep_state_and_data, [{reply, From, Result}]};
15111511

1512-
%% HTTP/3 socket operations
1512+
%% HTTP/3 socket operations — peername/sockname/peercert/setopts not exposed
1513+
%% by the underlying quic_h3 connection.
15131514
handle_common({call, From}, peername, _State,
15141515
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1515-
Result = hackney_quic:peername(H3Conn),
1516-
{keep_state_and_data, [{reply, From, Result}]};
1516+
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
15171517

15181518
handle_common({call, From}, sockname, _State,
15191519
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1520-
Result = hackney_quic:sockname(H3Conn),
1521-
{keep_state_and_data, [{reply, From, Result}]};
1520+
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
15221521

15231522
handle_common({call, From}, peercert, _State,
15241523
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1525-
%% QUIC/HTTP3 does not expose peer certificate through this API
15261524
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
15271525

1528-
handle_common({call, From}, {setopts, Opts}, _State,
1526+
handle_common({call, From}, {setopts, _Opts}, _State,
15291527
#conn_data{h3_conn = H3Conn} = _Data) when H3Conn =/= undefined ->
1530-
Result = hackney_quic:setopts(H3Conn, Opts),
1531-
{keep_state_and_data, [{reply, From, Result}]};
1528+
{keep_state_and_data, [{reply, From, {error, not_supported}}]};
15321529

15331530
%% Low-level send operation
15341531
handle_common({call, From}, {send, Data}, _State, #conn_data{transport = Transport, socket = Socket} = _Data)

src/hackney_h3.erl

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
send_body_chunk/4,
4040
finish_send_body/3,
4141
%% Stream management
42-
new_stream/1,
43-
close_stream/2,
4442
get_stream_state/2,
4543
update_stream_state/3,
4644
%% Response parsing
@@ -260,24 +258,19 @@ await_response(ConnRef, StreamId) ->
260258
-spec send_request(h3_conn(), method(), binary(), binary(), headers(), binary()) ->
261259
{ok, stream_id(), streams_map()} | {error, term()}.
262260
send_request(ConnRef, Method, Host, Path, Headers, Body) ->
263-
case hackney_quic:open_stream(ConnRef) of
264-
{ok, StreamId} ->
265-
AllHeaders = build_request_headers(Method, Host, Path, Headers),
266-
HasBody = Body =/= <<>> andalso Body =/= [],
267-
Fin = not HasBody,
268-
case hackney_quic:send_headers(ConnRef, StreamId, AllHeaders, Fin) of
269-
ok when HasBody ->
270-
case hackney_quic:send_data(ConnRef, StreamId, Body, true) of
271-
ok ->
272-
{ok, StreamId, #{StreamId => {undefined, waiting_headers}}};
273-
{error, _} = Error ->
274-
Error
275-
end;
261+
AllHeaders = build_request_headers(Method, Host, Path, Headers),
262+
HasBody = Body =/= <<>> andalso Body =/= [],
263+
Fin = not HasBody,
264+
case hackney_quic:send_request(ConnRef, AllHeaders, Fin) of
265+
{ok, StreamId} when HasBody ->
266+
case hackney_quic:send_data(ConnRef, StreamId, Body, true) of
276267
ok ->
277268
{ok, StreamId, #{StreamId => {undefined, waiting_headers}}};
278269
{error, _} = Error ->
279270
Error
280271
end;
272+
{ok, StreamId} ->
273+
{ok, StreamId, #{StreamId => {undefined, waiting_headers}}};
281274
{error, _} = Error ->
282275
Error
283276
end.
@@ -287,15 +280,10 @@ send_request(ConnRef, Method, Host, Path, Headers, Body) ->
287280
-spec send_request_headers(h3_conn(), method(), binary(), binary(), headers()) ->
288281
{ok, stream_id(), streams_map()} | {error, term()}.
289282
send_request_headers(ConnRef, Method, Host, Path, Headers) ->
290-
case hackney_quic:open_stream(ConnRef) of
283+
AllHeaders = build_request_headers(Method, Host, Path, Headers),
284+
case hackney_quic:send_request(ConnRef, AllHeaders, false) of
291285
{ok, StreamId} ->
292-
AllHeaders = build_request_headers(Method, Host, Path, Headers),
293-
case hackney_quic:send_headers(ConnRef, StreamId, AllHeaders, false) of
294-
ok ->
295-
{ok, StreamId, #{StreamId => {undefined, waiting_headers}}};
296-
{error, _} = Error ->
297-
Error
298-
end;
286+
{ok, StreamId, #{StreamId => {undefined, waiting_headers}}};
299287
{error, _} = Error ->
300288
Error
301289
end.
@@ -321,17 +309,6 @@ finish_send_body(ConnRef, StreamId, Streams) ->
321309
%% Stream management
322310
%%====================================================================
323311

324-
%% @doc Open a new stream for a request.
325-
-spec new_stream(h3_conn()) -> {ok, stream_id()} | {error, term()}.
326-
new_stream(ConnRef) ->
327-
hackney_quic:open_stream(ConnRef).
328-
329-
%% @doc Close a specific stream.
330-
-spec close_stream(h3_conn(), stream_id()) -> ok.
331-
close_stream(_ConnRef, _StreamId) ->
332-
%% HTTP/3 streams are closed when fin is sent/received
333-
ok.
334-
335312
%% @doc Get the state of a specific stream.
336313
-spec get_stream_state(stream_id(), streams_map()) ->
337314
{ok, {term(), stream_state()}} | {error, not_found}.
@@ -404,24 +381,19 @@ wait_connected(ConnRef, Timeout, StartTime) ->
404381
end.
405382

406383
do_request(ConnRef, Method, Host, Path, Headers, Body, Timeout) ->
407-
case hackney_quic:open_stream(ConnRef) of
408-
{ok, StreamId} ->
409-
AllHeaders = build_request_headers(Method, Host, Path, Headers),
410-
HasBody = Body =/= <<>> andalso Body =/= [],
411-
Fin = not HasBody,
412-
case hackney_quic:send_headers(ConnRef, StreamId, AllHeaders, Fin) of
413-
ok when HasBody ->
414-
case hackney_quic:send_data(ConnRef, StreamId, Body, true) of
415-
ok ->
416-
await_response_loop(ConnRef, StreamId, Timeout, undefined, [], <<>>);
417-
{error, _} = Error ->
418-
Error
419-
end;
384+
AllHeaders = build_request_headers(Method, Host, Path, Headers),
385+
HasBody = Body =/= <<>> andalso Body =/= [],
386+
Fin = not HasBody,
387+
case hackney_quic:send_request(ConnRef, AllHeaders, Fin) of
388+
{ok, StreamId} when HasBody ->
389+
case hackney_quic:send_data(ConnRef, StreamId, Body, true) of
420390
ok ->
421391
await_response_loop(ConnRef, StreamId, Timeout, undefined, [], <<>>);
422392
{error, _} = Error ->
423393
Error
424394
end;
395+
{ok, StreamId} ->
396+
await_response_loop(ConnRef, StreamId, Timeout, undefined, [], <<>>);
425397
{error, _} = Error ->
426398
Error
427399
end.

0 commit comments

Comments
 (0)