Packages
hackney
2.0.0-beta.1
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
src/hackney_quic.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2024-2026 Benoit Chesneau
%%%
%%% @doc QUIC NIF wrapper for HTTP/3 support.
%%%
%%% This module provides the Erlang interface to the QUIC NIF.
%%% The NIF handles QUIC transport and HTTP/3 using lsquic.
%%%
%%% == Connection Options ==
%%%
%%% The `Opts' map passed to `connect/4' may contain:
%%% <ul>
%%% <li>`socket_fd' - An existing UDP socket file descriptor (integer).
%%% If provided, the NIF will use this socket instead of creating
%%% a new one. This allows pre-warming connections and H3 detection
%%% in Erlang before handing off to the NIF. Use `get_fd/1' to
%%% extract the FD from a gen_udp socket.</li>
%%% <li>`verify' - Boolean indicating whether to verify server certificate
%%% (default: false)</li>
%%% </ul>
%%%
%%% == Messages ==
%%%
%%% Messages sent from NIF to owner process:
%%% <ul>
%%% <li>`{quic, ConnRef, {connected, Info}}' - Connection established</li>
%%% <li>`{quic, ConnRef, {stream_opened, StreamId}}' - Stream opened</li>
%%% <li>`{quic, ConnRef, {closed, Reason}}' - Connection closed</li>
%%% <li>`{quic, ConnRef, {transport_error, Code, Reason}}' - Transport error</li>
%%% <li>`{quic, ConnRef, {stream_headers, StreamId, Headers, Fin}}' - Headers received</li>
%%% <li>`{quic, ConnRef, {stream_data, StreamId, Bin, Fin}}' - Data received</li>
%%% <li>`{quic, ConnRef, {stream_reset, StreamId, ErrorCode}}' - Stream reset</li>
%%% <li>`{quic, ConnRef, {stop_sending, StreamId, ErrorCode}}' - Stop sending</li>
%%% <li>`{quic, ConnRef, {goaway, LastStreamId, ErrorCode, Debug}}' - GoAway received</li>
%%% <li>`{quic, ConnRef, {session_ticket, Ticket}}' - Session ticket for 0-RTT</li>
%%% <li>`{quic, ConnRef, {send_ready, StreamId}}' - Stream ready to write</li>
%%% <li>`{quic, ConnRef, {timer, NextTimeoutMs}}' - Timer notification</li>
%%% </ul>
-module(hackney_quic).
-export([
connect/4,
close/2,
open_stream/1,
send_headers/4,
send_data/4,
reset_stream/3,
handle_timeout/2,
process/1,
peername/1,
sockname/1,
setopts/2
]).
-export([is_available/0, get_fd/1]).
-on_load(init/0).
-define(NIF_NOT_LOADED, erlang:nif_error(nif_not_loaded)).
%%====================================================================
%% NIF Loading
%%====================================================================
init() ->
PrivDir = case code:priv_dir(hackney) of
{error, bad_name} ->
%% Fallback for development
case filelib:is_dir(filename:join(["..", "priv"])) of
true -> filename:join(["..", "priv"]);
false -> "priv"
end;
Dir -> Dir
end,
SoName = filename:join(PrivDir, "hackney_quic"),
case erlang:load_nif(SoName, 0) of
ok -> ok;
{error, {load_failed, _}} -> ok; % NIF not built yet
{error, {reload, _}} -> ok;
{error, Reason} ->
error_logger:warning_msg("Failed to load hackney_quic NIF: ~p~n", [Reason]),
ok
end.
%%====================================================================
%% API
%%====================================================================
%% @doc Check if QUIC/HTTP3 support is available.
%% Returns true if the NIF is loaded and ready.
-spec is_available() -> boolean().
is_available() ->
try
%% Try to call connect - if NIF is loaded it will return {ok, Ref}
%% or {error, _}. If NIF is not loaded it will throw nif_not_loaded.
case connect(<<"test">>, 443, #{}, self()) of
{ok, ConnRef} ->
%% NIF is loaded, clean up the test connection
catch close(ConnRef, normal),
true;
{error, _} ->
%% NIF is loaded, just failed (expected for invalid params)
true
end
catch
error:nif_not_loaded -> false;
_:_ -> false
end.
%% @doc Get the file descriptor from a gen_udp socket.
%% This can be used to pass an existing UDP socket to the QUIC NIF
%% via the `socket_fd' option.
%%
%% Example:
%% ```
%% {ok, Socket} = gen_udp:open(0, [binary, {active, false}]),
%% {ok, Fd} = hackney_quic:get_fd(Socket),
%% {ok, ConnRef} = hackney_quic:connect(Host, Port, #{socket_fd => Fd}, self()).
%% '''
%%
%% Note: After passing the FD to the NIF, do NOT close the gen_udp socket
%% as the NIF now owns the file descriptor. The socket will be closed
%% when the QUIC connection is closed.
-spec get_fd(gen_udp:socket()) -> {ok, integer()} | {error, term()}.
get_fd(Socket) ->
case inet:getfd(Socket) of
{ok, Fd} -> {ok, Fd};
Error -> Error
end.
%% @doc Connect to a QUIC server.
%% Returns {ok, ConnRef} on success.
%% The owner process will receive {quic, ConnRef, {connected, Info}}
%% when the connection is established.
%%
%% Options:
%% <ul>
%% <li>`socket_fd' - Use an existing UDP socket FD (see `get_fd/1')</li>
%% <li>`verify' - Verify server certificate (default: false)</li>
%% </ul>
-spec connect(Host, Port, Opts, Owner) -> {ok, reference()} | {error, term()}
when Host :: binary() | string(),
Port :: inet:port_number(),
Opts :: map(),
Owner :: pid().
connect(Host, Port, Opts, Owner) when is_list(Host) ->
connect(list_to_binary(Host), Port, Opts, Owner);
connect(Host, Port, Opts, Owner) when is_binary(Host), is_integer(Port),
Port > 0, Port =< 65535,
is_map(Opts), is_pid(Owner) ->
connect_nif(Host, Port, Opts, Owner);
connect(_Host, _Port, _Opts, _Owner) ->
{error, badarg}.
connect_nif(_Host, _Port, _Opts, _Owner) ->
?NIF_NOT_LOADED.
%% @doc Close a QUIC connection.
-spec close(ConnRef, Reason) -> ok
when ConnRef :: reference(),
Reason :: term().
close(ConnRef, Reason) ->
close_nif(ConnRef, Reason).
close_nif(_ConnRef, _Reason) ->
?NIF_NOT_LOADED.
%% @doc Open a new bidirectional stream.
%% Returns {ok, StreamId} on success. The StreamId may be 0 if the stream
%% creation is pending; the actual stream ID will be provided via the
%% on_new_stream callback message.
-spec open_stream(ConnRef) -> {ok, non_neg_integer()} | {error, term()}
when ConnRef :: reference().
open_stream(ConnRef) ->
open_stream_nif(ConnRef).
open_stream_nif(_ConnRef) ->
?NIF_NOT_LOADED.
%% @doc Send HTTP/3 headers on a stream.
%% Headers should be [{Name, Value}] with binary keys/values.
%% Fin indicates if this is the final frame on the stream.
-spec send_headers(ConnRef, StreamId, Headers, Fin) -> ok | {error, term()}
when ConnRef :: reference(),
StreamId :: non_neg_integer(),
Headers :: [{binary(), binary()}],
Fin :: boolean().
send_headers(ConnRef, StreamId, Headers, Fin) when is_list(Headers), is_boolean(Fin) ->
send_headers_nif(ConnRef, StreamId, Headers, Fin);
send_headers(_ConnRef, _StreamId, _Headers, _Fin) ->
{error, badarg}.
send_headers_nif(_ConnRef, _StreamId, _Headers, _Fin) ->
?NIF_NOT_LOADED.
%% @doc Send data on a stream.
%% Fin indicates if this is the final frame on the stream.
-spec send_data(ConnRef, StreamId, Data, Fin) -> ok | {error, term()}
when ConnRef :: reference(),
StreamId :: non_neg_integer(),
Data :: iodata(),
Fin :: boolean().
send_data(ConnRef, StreamId, Data, Fin) when is_boolean(Fin) ->
send_data_nif(ConnRef, StreamId, Data, Fin);
send_data(_ConnRef, _StreamId, _Data, _Fin) ->
{error, badarg}.
send_data_nif(_ConnRef, _StreamId, _Data, _Fin) ->
?NIF_NOT_LOADED.
%% @doc Reset a stream with an error code.
-spec reset_stream(ConnRef, StreamId, ErrorCode) -> ok | {error, term()}
when ConnRef :: reference(),
StreamId :: non_neg_integer(),
ErrorCode :: non_neg_integer().
reset_stream(ConnRef, StreamId, ErrorCode) when is_integer(ErrorCode), ErrorCode >= 0 ->
reset_stream_nif(ConnRef, StreamId, ErrorCode);
reset_stream(_ConnRef, _StreamId, _ErrorCode) ->
{error, badarg}.
reset_stream_nif(_ConnRef, _StreamId, _ErrorCode) ->
?NIF_NOT_LOADED.
%% @doc Handle connection timeout.
%% Should be called when timer expires.
%% Returns next timeout in ms or 'infinity'.
-spec handle_timeout(ConnRef, NowMs) -> non_neg_integer() | infinity
when ConnRef :: reference(),
NowMs :: non_neg_integer().
handle_timeout(ConnRef, NowMs) when is_integer(NowMs) ->
handle_timeout_nif(ConnRef, NowMs);
handle_timeout(_ConnRef, _NowMs) ->
infinity.
handle_timeout_nif(_ConnRef, _NowMs) ->
?NIF_NOT_LOADED.
%% @doc Process pending QUIC events.
%% This should be called when:
%% <ul>
%% <li>The socket has data ready (after receiving `{select, _, _, ready_input}')</li>
%% <li>A timer has expired</li>
%% </ul>
%% Returns the next timeout in milliseconds, or 'infinity' if no timeout needed.
%% The caller should use `erlang:send_after/3' to schedule the next call.
%%
%% Example usage:
%% ```
%% receive
%% {select, _Resource, _Ref, ready_input} ->
%% NextTimeout = hackney_quic:process(ConnRef),
%% schedule_timer(NextTimeout)
%% end
%% '''
-spec process(ConnRef) -> non_neg_integer() | infinity
when ConnRef :: reference().
process(ConnRef) ->
process_nif(ConnRef).
process_nif(_ConnRef) ->
?NIF_NOT_LOADED.
%% @doc Get the remote address of the connection.
-spec peername(ConnRef) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}
when ConnRef :: reference().
peername(ConnRef) ->
peername_nif(ConnRef).
peername_nif(_ConnRef) ->
?NIF_NOT_LOADED.
%% @doc Get the local address of the connection.
-spec sockname(ConnRef) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}
when ConnRef :: reference().
sockname(ConnRef) ->
sockname_nif(ConnRef).
sockname_nif(_ConnRef) ->
?NIF_NOT_LOADED.
%% @doc Set connection options.
-spec setopts(ConnRef, Opts) -> ok | {error, term()}
when ConnRef :: reference(),
Opts :: [{atom(), term()}].
setopts(ConnRef, Opts) when is_list(Opts) ->
setopts_nif(ConnRef, Opts);
setopts(_ConnRef, _Opts) ->
{error, badarg}.
setopts_nif(_ConnRef, _Opts) ->
?NIF_NOT_LOADED.