13 KiB
HTTP/3 transport
Uncompiled development source in this environment. This release adds an actual QUIC/HTTP/3 adapter, not only an Alt-Svc header. It does not claim independent full-RFC conformance, interoperability or production performance until the acceptance tests run.
Stack and configuration
The adapter delegates QUIC transport/TLS to Quinn 0.11.12 and HTTP/3/QPACK plus the
Quinn adapter to hpx-h3 2.5.20 with its quinn feature. The crate is imported as h3
in source. These are Cargo dependencies, not bundled binaries.
Rustls provides TLS 1.3 for QUIC; SNI certificates come from the existing inventory.
HTTP/1.1 and optional HTTP/2 remain on TCP at the same port. HTTP/3 is on UDP.
There is no HTTP/3 upstream client in this revision: origins still use HTTP/1.1 or HTTP/2.
Each vhost has http3_enabled (default true). When false, RustProxy removes
Alt-Svc from responses for that vhost and rejects HTTP/3 requests for it with 421,
while HTTP/1.1 and any permitted HTTP/2 traffic on the selected TLS listener keep
working. The UDP listener is still a listener-level resource; the vhost switch narrows
use of that shared capability rather than hot-unbinding the UDP socket.
HTTP/2 follows the same UI model through http2_enabled (default true). A vhost may
only opt into H2 when one of its selected named listeners enables H2. For TLS, RustProxy
reads ClientHello/SNI before finishing the handshake and selects H2+HTTP/1.1 or
HTTP/1.1-only ALPN for that vhost. The listener remains the capability ceiling.
Since 0.5.0, an omitted http3 enables QUIC with defaults on TLS bindings.
Explicit http3: null disables it and is preserved when serialized. An object
enables it with chosen limits. Plain HTTP defaults to no QUIC. In 0.24.3 the
Enable HTTP/3 over QUIC (UDP) switch is edited with the TLS endpoint under
Settings -> Listeners. The listener row links to Settings -> HTTP connections
-> HTTP/3, which contains only advertisement, connection, stream and idle-timeout
tuning for listeners where H3 is enabled. Both enablement and tuning remain
restart-required. Configure this only on named listeners.endpoints[].binding:
{
"address": "127.0.0.1:8443",
"tls": true,
"http2": true,
"http3": {
"alt_svc_max_age": 3600,
"max_concurrent_connections": 512,
"max_concurrent_bidistream": 100,
"max_concurrent_unistream": 100,
"max_idle_timeout": 10
}
}
| Field | Meaning and accepted limits |
|---|---|
| alt_svc_max_age | Advertisement lifetime, seconds, 0..604800 |
| max_concurrent_connections | Established/pending accepted connection slots per endpoint, 1..4096 |
| max_concurrent_bidistream | Peer bidirectional streams per connection, 1..1024 |
| max_concurrent_unistream | Peer unidirectional streams per connection, 3..1024 |
| max_idle_timeout | QUIC idle timeout, seconds, 0..86400; zero means no local QUIC idle timeout |
Three unidirectional streams are required for the peer control and QPACK streams. These stream limits are NOT global to all listeners. The existing global in-flight request and application body/header deadlines still apply. Setting QUIC idle to zero does not disable application deadlines, and the peer can negotiate its own idle timeout. All changes to socket/ALPN/QUIC transport parameters require restart.
Transport behavior and safety
- Bind UDP before announcing readiness; failure to bind is a startup failure.
- Advertise
Alt-Svc: h3=":port"; ma=Nonly on an ACTUAL active TLS+H3 endpoint. Origin Alt-Svc is stripped so it cannot advertise the origin or a disabled service. - UDP address validation/Retry, bounded pending handshakes, a 10-second handshake deadline, per-endpoint permits, stream flow control and bounded application concurrency.
- 32 KiB decoded HTTP field section limit; request content-length checks and rejection of forbidden connection/framing headers; streamed bodies and trailers.
- Shared routing pipeline: real local socket -> validated authority -> trusted client address -> ACL -> Tinyauth -> cache -> origin -> response/security/Server policies.
- HTTP cache semantics are shared with HTTP/1.1 and HTTP/2. In 0.27.3, body-less
HEADrequests use the same bounded empty-body probe asGET, so an initially unpolled HTTP/3 receive stream does not cause a falserequest_bodycache bypass. - 0-RTT is disabled to avoid replaying writes; no retry to another origin or transport.
- Endpoint active migration is disabled. Missing real local destination data on a wildcard QUIC connection fails closed rather than substituting a forwarded IP.
- Configuration reload changes SNI certificates for new QUIC handshakes without changing active socket limits. Graceful shutdown requests GOAWAY then bounded drain.
- Counters: http3_requests, http3_connections, http3_active_connections and http3_rejected_connections in status/Prometheus; H3 request count also in dashboard.
A connection receives a maximum aggregate 8 MiB QUIC window and 256 KiB per-stream
window; these limits are not process memory guarantees. Many simultaneous connections
can use substantial memory. Size connection/stream limits to the machine and measure.
No active path migration, 0-RTT, server push, WebTransport or HTTP datagrams are enabled.
RFC 9220 WebSocket Extended CONNECT is experimental and disabled by default per vhost.
RustProxy advertises SETTINGS_ENABLE_CONNECT_PROTOCOL on a QUIC listener only when at least
one vhost bound to that listener explicitly enables websocket_rfc9220; request routing
still rejects RFC 9220 for vhosts that did not opt in. Generic CONNECT and other Extended
CONNECT protocols remain unsupported and are answered with 501. The downstream H3 request must
not carry HTTP/1.1-only Connection, Upgrade, Sec-WebSocket-Key or
Sec-WebSocket-Accept fields. RustProxy creates a private RFC 6455 key for the HTTP/1.1
backend handshake; a valid backend 101 becomes downstream 200, after which H3 DATA
bytes are the WebSocket byte stream. Client FIN half-closes the backend write side;
backend EOF finishes the H3 send side; abnormal/forced termination uses
H3_REQUEST_CANCELLED. No side TCP listener or protocol bypass is used.
WebSocket bootstrap ordering and diagnostics in 0.16.0
For RFC 9220, a validated backend 101 is no longer enough to send the client-facing H3 success immediately. RustProxy first awaits Hyper's backend OnUpgrade and obtains the actual upgraded stream. Only then is downstream 200 returned and the H3 DATA bridge started. If backend takeover is cancelled or fails, the client receives a normal 503/502 response instead of 200 followed by an immediate H3_REQUEST_CANCELLED reset.
Access logging can now distinguish this path with http_version, transport, websocket_bootstrap, websocket_phase, websocket_error, h3_reset_code and upstream_status. This makes Safari/iOS interoperability failures distinguishable from backend Socket.IO responses and from H3 tunnel resets. Query-string logging remains subject to the normal privacy policy; do not use access logging to capture session secrets.
The advertised port is the real listener port. Behind port-rewriting NAT, use a matching externally reachable UDP/TCP port or do not enable this advertisement; there is no separate external-port override. Open UDP as well as TCP in your firewall/load balancer. Scope labels do not perform those operations. Clients lacking QUIC or blocked UDP can still connect using TCP; no guarantee is made about browser protocol preference.
H3 full-page navigation to HTTP/1.1 backend (0.31.6)
The RustProxy logo remains a normal href="/" link, not a client-side panel change. On an H3-enabled frontend it produces a new document GET through the proxy. When forwarding an H3 GET/HEAD to an HTTP/1.1 backend, RustProxy now probes for the stream's end for at most 35 ms per initial frame. A finished empty request is passed to H1 with a confirmed complete receive half rather than an unknown body state. Hyper already special-cases H1 GET/HEAD framing; no claim is made that ordinary GET requests were chunked. A nonempty frame and all subsequent bytes are replayed without buffering the whole request. The slow-peer case falls back to regular streaming.
If h3.accept() fails, existing request tasks get the normal bounded drain instead of immediate mass abort. The new diagnostics use status=0 for cancellations (not a real HTTP response), and persist interrupted-response details including phase=, stream_id=, method=, path=, response_status=, body_bytes_sent= and error= in the bounded, redacted error_detail. These are stored per vhost when file logging is enabled; the normal access logs remain available for successful document and asset GETs.
iOS validation procedure: keep H3 on, click the logo repeatedly on an authenticated panel behind the real RustProxy vhost, and collect timestamps for http3_peer_cancelled_before_headers, http3_peer_cancelled_after_headers, http3_response_write_error, http3_accept_error, and the GET / and asset access records. Collect browser Web Inspector Network/QUIC trace if a blank document persists. Correlate the new GET/ status and transmitted bytes with older cancelled streams. Mere cancellation of an obsolete API or SSE stream is not proof that the new document failed. This implementation is not proven to resolve an iOS issue until live tests pass.
Browser refresh and stream cancellation (0.31.5)
A browser may cancel an obsolete H3 stream when the user reloads or leaves a
page, sometimes before its HEADERS arrive. RFC 9114 H3_REQUEST_CANCELLED
(and a peer-originated H3_NO_ERROR reset) is not itself a server-side HTTP 400.
RustProxy therefore omits that fabricated 400 access-log entry and avoids
adding H3_INTERNAL_ERROR to a directly identified cancelled response stream.
Malformed or forbidden headers still yield a protocol-error 400.
This correction does not establish the cause of a blank document after F5. If the new navigation fails, compare its actual document/asset statuses over H3 and H2, the configured reverse-proxy Host/Origin/forwarded-proto chain, and any connection-level QUIC errors. An error record without HTTP method, path and request headers cannot identify the browser document which failed. Capture browser network traces and QUIC qlogs with timestamps for live acceptance.
Per-vhost http2_enabled/http3_enabled toggles also no longer rotate the global
HOT/COLD cache identity. Origin representation policy and cache invalidation are
unchanged; the frontend protocol remains part of live request routing.
Verification gate
bash dev.sh check
cargo test --locked --test http3 -- --nocapture
cargo test --locked --test integrations --test listeners
bash dev.sh test
tests/http3.rs creates a real loopback UDP endpoint, a trusted generated test
certificate, a real origin and a Quinn+h3 client. It checks MISS/HIT, body preservation,
Server/diagnostic policy, connection capacity and ALPN HTTP/2/HTTP/1.1 compatibility.
The same integration test explicitly enables websocket_rfc9220, then opens an RFC 9220 Extended CONNECT stream and translates it
to a real HTTP/1.1 backend Upgrade, sends a masked WebSocket frame as H3 DATA, half-closes
the client send side and verifies byte-transparent DATA return plus tunnel metrics. Unit
regressions separately reject HTTP/1.1-only proof/framing headers on the H3 side.
It is provided but was NOT run in the packaging environment. The same-stack client
is a regression test, not independent interoperability evidence.
After applying examples/http3-vhost.json on a disposable test instance and starting
bash dev.sh mock --port 9001, use an independently built curl with HTTP3 support:
curl --version
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http3-only --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http2 --cacert data/certs/default.pem -D - https://localhost:8443/cache
curl --http1.1 --cacert data/certs/default.pem -D - https://localhost:8443/cache
Do not substitute --http3 fallback or -k for strict protocol/certificate validation.
Also test independent clients, malformed-frame handling, stream exhaustion, slow
uploads/downloads, IPv6 wildcard destination metadata, cancellation, graceful drain,
certificate rotation, and memory/CPU at configured maxima before exposure.
No full RFC conformance or load-test result is asserted by this source package.
Primary references
- HTTP/3 RFC 9114: https://www.rfc-editor.org/rfc/rfc9114.html
- QUIC RFC 9000: https://www.rfc-editor.org/rfc/rfc9000.html
- QPACK RFC 9204: https://www.rfc-editor.org/rfc/rfc9204.html
- Quinn: https://docs.rs/quinn/latest/quinn/
- RFC 8441 Extended CONNECT: https://www.rfc-editor.org/rfc/rfc8441.html
- RFC 9220 WebSocket over HTTP/3: https://www.rfc-editor.org/rfc/rfc9220.html
- hpx-h3 2.5.19: https://docs.rs/hpx-h3/2.5.19/h3/