# HTTP/3 Guide
This guide covers hackney's HTTP/3 support via QUIC.
## Overview
Hackney supports HTTP/3, the latest version of HTTP built on QUIC (UDP-based transport). HTTP/3 offers improved performance, especially on lossy networks, with features like connection migration and zero round-trip connection establishment.
### Key Features
- **QUIC transport** - UDP-based, encrypted by default with TLS 1.3
- **Transparent API** - Same `hackney:get/post/request` functions work for HTTP/3
- **Multiplexing** - Multiple streams without head-of-line blocking
- **Alt-Svc discovery** - Automatic HTTP/3 endpoint detection from Alt-Svc headers
- **Connection pooling** - HTTP/3 connections shared across callers
- **Negative caching** - Failed H3 attempts cached to avoid repeated failures
## Requirements
HTTP/3 support requires the QUIC NIF to be built. Check availability:
```erlang
hackney_quic:is_available(). %% true | false
```
The NIF is built automatically with `rebar3 compile` if dependencies (cmake, zlib) are available.
## Quick Start
```erlang
%% HTTP/3 request with explicit protocol selection
{ok, 200, Headers, Body} = hackney:get(
<<"https://cloudflare.com/cdn-cgi/trace">>,
[],
<<>>,
[{protocols, [http3]}, with_body]
).
%% Body contains: http=http/3
```
## Protocol Selection
### Default Behavior
By default, hackney uses HTTP/2 and HTTP/1.1 (not HTTP/3):
```erlang
%% Default: [http2, http1]
hackney:get(<<"https://example.com/">>).
```
### Enable HTTP/3
Add `http3` to the protocols list:
```erlang
%% Try HTTP/3 first, fall back to HTTP/2, then HTTP/1.1
hackney:get(URL, [], <<>>, [{protocols, [http3, http2, http1]}]).
```
### Force HTTP/3 Only
```erlang
%% HTTP/3 only - fails if H3 unavailable
hackney:get(URL, [], <<>>, [{protocols, [http3]}]).
```
### Force HTTP/2 Only
```erlang
hackney:get(URL, [], <<>>, [{protocols, [http2]}]).
```
### Force HTTP/1.1 Only
```erlang
hackney:get(URL, [], <<>>, [{protocols, [http1]}]).
```
## Detecting the Protocol
Check the negotiated protocol on a connection:
```erlang
{ok, Conn} = hackney:connect(hackney_ssl, "cloudflare.com", 443,
[{protocols, [http3]}]),
Protocol = hackney_conn:get_protocol(Conn). %% http3 | http2 | http1
hackney:close(Conn).
```
Or verify via Cloudflare's trace endpoint:
```erlang
{ok, 200, _, Body} = hackney:get(
<<"https://cloudflare.com/cdn-cgi/trace">>,
[], <<>>,
[{protocols, [http3]}, with_body]
),
%% Body contains "http=http/3" if using HTTP/3
```
## Alt-Svc Discovery
Servers advertise HTTP/3 support via the `Alt-Svc` response header:
```
Alt-Svc: h3=":443"; ma=86400
```
Hackney automatically caches these and uses HTTP/3 on subsequent requests:
```erlang
%% First request uses HTTP/2 or HTTP/1.1
%% Server returns Alt-Svc: h3=":443"; ma=86400
{ok, _, Headers1, _} = hackney:get(URL, [], <<>>, [{protocols, [http3, http2, http1]}]).
%% Alt-Svc is now cached, second request uses HTTP/3
{ok, _, Headers2, _} = hackney:get(URL, [], <<>>, [{protocols, [http3, http2, http1]}]).
```
### Manual Alt-Svc Cache Management
```erlang
%% Check if HTTP/3 is cached for a host
hackney_altsvc:lookup(<<"example.com">>, 443).
%% {ok, h3, 443} | none
%% Manually cache HTTP/3 endpoint
hackney_altsvc:cache(<<"example.com">>, 443, 443, 86400).
%% Clear cached entry
hackney_altsvc:clear(<<"example.com">>, 443).
%% Clear all cached entries
hackney_altsvc:clear_all().
```
## Connection Multiplexing
Like HTTP/2, HTTP/3 multiplexes requests as streams on a single QUIC connection:
```erlang
%% All requests share ONE QUIC connection
{ok, _, _, _} = hackney:get(<<"https://cloudflare.com/">>,
[], <<>>, [{protocols, [http3]}]).
{ok, _, _, _} = hackney:get(<<"https://cloudflare.com/cdn-cgi/trace">>,
[], <<>>, [{protocols, [http3]}]).
```
### Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ hackney_pool │
│ │
│ h3_connections = #{ {Host, Port, Transport} => Pid } │
│ │
│ checkout_h3(Host, Port, ...) -> │
│ case maps:get(Key, h3_connections) of │
│ Pid -> {ok, Pid}; %% Reuse existing │
│ undefined -> none %% Create new │
│ end │
│ │
│ register_h3(Host, Port, ..., Pid) -> │
│ h3_connections#{Key => Pid} %% Store for reuse │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ hackney_conn (gen_statem process) │
│ │
│ h3_conn = <QUIC connection reference> │
│ │
│ h3_streams = #{ │
│ 0 => {CallerA, waiting_headers, <<>>}, │
│ 4 => {CallerB, waiting_headers, <<>>}, │
│ 8 => {CallerC, waiting_headers, <<>>} │
│ } │
│ │
│ Request from CallerA → open_stream() → StreamId=0 │
│ Request from CallerB → open_stream() → StreamId=4 │
│ Request from CallerC → open_stream() → StreamId=8 │
│ │
│ Response for StreamId=4 arrives: │
│ → lookup h3_streams[4] → CallerB │
│ → gen_statem:reply(CallerB, {ok, Status, Headers, Body}) │
└─────────────────────────────────────────────────────────────────┘
```
## UDP Blocking and Fallback
Some networks block UDP traffic, which prevents HTTP/3 from working. Hackney handles this with negative caching:
```erlang
%% If HTTP/3 fails, host is marked as blocked for 5 minutes
%% Subsequent requests skip HTTP/3 and use HTTP/2 or HTTP/1.1
%% Check if host is marked as H3-blocked
hackney_altsvc:is_h3_blocked(<<"example.com">>, 443). %% true | false
%% Manually mark as blocked (e.g., for testing)
hackney_altsvc:mark_h3_blocked(<<"example.com">>, 443).
```
## HTTP/3 vs HTTP/2 Differences
| Feature | HTTP/3 | HTTP/2 |
|---------|--------|--------|
| Transport | QUIC (UDP) | TCP |
| TLS | Built-in (TLS 1.3) | Separate layer |
| Head-of-line blocking | Per-stream only | Connection-wide |
| Connection migration | Supported | Not supported |
| 0-RTT resumption | Supported | Not supported |
### Header Format
Both HTTP/2 and HTTP/3 use lowercase header names:
```erlang
%% HTTP/3 headers (same as HTTP/2)
[{<<":status">>, <<"200">>},
{<<"content-type">>, <<"text/html">>},
{<<"server">>, <<"cloudflare">>}]
```
## Error Handling
```erlang
case hackney:get(URL, [], <<>>, [{protocols, [http3]}]) of
{ok, Status, Headers, Body} ->
ok;
{error, {quic_error, Code, Reason}} ->
%% QUIC-level error
io:format("QUIC error ~p: ~s~n", [Code, Reason]);
{error, timeout} ->
%% Connection timeout (possibly UDP blocked)
io:format("Timeout - UDP may be blocked~n");
{error, Reason} ->
io:format("Error: ~p~n", [Reason])
end.
```
## Performance Tips
### Use HTTP/3 for Unreliable Networks
HTTP/3's per-stream flow control and connection migration work well on mobile or lossy networks:
```erlang
%% Good for mobile apps
Opts = [{protocols, [http3, http2, http1]}, {connect_timeout, 10000}].
```
### Connection Reuse
HTTP/3 connections are expensive to establish. Use pooling:
```erlang
%% Good: connections are reused via pool
[hackney:get(URL, [], <<>>, [{pool, default}, {protocols, [http3]}])
|| _ <- lists:seq(1, 100)].
%% Bad: new QUIC handshake each time
[hackney:get(URL, [], <<>>, [{pool, false}, {protocols, [http3]}])
|| _ <- lists:seq(1, 100)].
```
## Compatibility
### Server Requirements
HTTP/3 requires servers that support:
- QUIC (RFC 9000)
- HTTP/3 (RFC 9114)
Major CDNs with HTTP/3 support:
- Cloudflare
- Google
- Fastly
- Akamai
### Checking Server Support
```bash
# Using curl
curl -v --http3 https://cloudflare.com/ 2>&1 | grep -i http/3
# Check Alt-Svc header
curl -v https://cloudflare.com/ 2>&1 | grep -i alt-svc
```
### Fallback
If HTTP/3 is unavailable, hackney falls back to HTTP/2 or HTTP/1.1:
```erlang
%% Works regardless of H3 support (if http2/http1 in protocols)
{ok, _, _, _} = hackney:get(URL, [], <<>>,
[{protocols, [http3, http2, http1]}]).
```
## Examples
### Elixir
```elixir
# Start hackney
Application.ensure_all_started(:hackney)
# HTTP/3 request
{:ok, status, headers, body} = :hackney.get(
"https://cloudflare.com/cdn-cgi/trace",
[],
"",
[{:protocols, [:http3]}, :with_body]
)
# Verify HTTP/3
String.contains?(body, "http=http/3") # true
```
### Force Protocol
```erlang
%% HTTP/3 only - fails if server doesn't support it or UDP blocked
{ok, _, _, _} = hackney:get(URL, [], <<>>, [
with_body,
{protocols, [http3]}
]).
%% HTTP/2 only - never uses HTTP/3
{ok, _, _, _} = hackney:get(URL, [], <<>>, [
with_body,
{protocols, [http2]}
]).
```
## Troubleshooting
### HTTP/3 Not Being Used
1. Check if QUIC NIF is loaded:
```erlang
hackney_quic:is_available(). %% Should be true
```
2. Check if `http3` is in protocols list
3. Check if host is marked as blocked:
```erlang
hackney_altsvc:is_h3_blocked(Host, Port).
```
4. Verify server supports HTTP/3:
```bash
curl -v --http3 https://example.com/
```
### Connection Timeouts
UDP may be blocked by firewalls. Try:
1. Use fallback protocols: `{protocols, [http3, http2, http1]}`
2. Check if other HTTP/3 sites work (e.g., cloudflare.com)
3. Check firewall/network settings for UDP port 443
### Build Issues
If QUIC NIF fails to build:
1. Ensure cmake is installed
2. Ensure zlib development headers are available
3. Check build output for errors:
```bash
rebar3 compile 2>&1 | grep -i error
```
## Next Steps
- [HTTP/2 Guide](http2_guide.md) - HTTP/2 features
- [HTTP Guide](http_guide.md) - General HTTP features
- [Design Guide](design.md) - Architecture details