Skip to content

Commit c04a4f7

Browse files
committed
quic: add http/3 details to doc
1 parent cf3d050 commit c04a4f7

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

doc/api/quic.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2862,6 +2862,159 @@ added: REPLACEME
28622862
Called when informational (1xx) headers are received from the server
28632863
(e.g., 103 Early Hints).
28642864

2865+
## HTTP/3 support
2866+
2867+
<!-- YAML
2868+
added: REPLACEME
2869+
-->
2870+
2871+
When the negotiated ALPN identifier is `'h3'` (or one of the `'h3-*'`
2872+
draft variants), the QUIC session runs the HTTP/3 application backed
2873+
by `nghttp3`. `'h3'` is the default ALPN for `quic.connect()` and
2874+
`quic.listen()`, so HTTP/3 is what you get unless you select a
2875+
different ALPN explicitly.
2876+
2877+
Selecting the HTTP/3 application enables a number of stream- and
2878+
session-level capabilities that are not available to non-HTTP/3
2879+
applications:
2880+
2881+
* **Headers and trailers** — request and response header blocks
2882+
(including pseudo-headers such as `:method`, `:path`, `:scheme`,
2883+
`:authority`, and `:status`), trailing headers, and informational
2884+
(`1xx`) responses. See [`stream.sendHeaders()`][],
2885+
[`stream.sendTrailers()`][], and
2886+
[`stream.sendInformationalHeaders()`][].
2887+
* **Stream priority (RFC 9218)** — per-stream urgency and
2888+
incremental flags. See [`stream.priority`][] and
2889+
[`stream.setPriority()`][].
2890+
* **HTTP/3 datagrams (RFC 9297)** — unreliable application-layer
2891+
datagrams. The peer must advertise `SETTINGS_H3_DATAGRAM=1`, which
2892+
is enabled by setting [`application.enableDatagrams`][] to `true`
2893+
on both peers. See [`session.sendDatagram()`][] and
2894+
[`session.ondatagram`][].
2895+
* **ORIGIN frame (RFC 9412)** — servers automatically advertise the
2896+
hostnames in their [`sessionOptions.sni`][] map (entries with
2897+
`authoritative: true`); clients receive the list via
2898+
[`session.onorigin`][].
2899+
* **GOAWAY** — graceful shutdown. The server emits `GOAWAY` as part
2900+
of [`session.close()`][]; the client observes it via
2901+
[`session.ongoaway`][] and stops opening new bidirectional streams.
2902+
* **Extended CONNECT settings (RFC 9220)** — the
2903+
`SETTINGS_ENABLE_CONNECT_PROTOCOL` setting can be enabled via
2904+
[`application.enableConnectProtocol`][]. The setting is exchanged
2905+
but the application is responsible for handling the `:protocol`
2906+
pseudo-header and any payload framing on top.
2907+
* **QPACK tuning** — dynamic-table size and blocked-streams limits
2908+
via [`application.qpackMaxDTableCapacity`][] and friends.
2909+
2910+
### Minimal HTTP/3 client
2911+
2912+
```mjs
2913+
import { connect } from 'node:quic';
2914+
import process from 'node:process';
2915+
2916+
const session = await connect('example.com:443', {
2917+
// ALPN defaults to 'h3'.
2918+
servername: 'example.com',
2919+
});
2920+
await session.opened;
2921+
2922+
const stream = await session.createBidirectionalStream({
2923+
headers: {
2924+
':method': 'GET',
2925+
':path': '/',
2926+
':scheme': 'https',
2927+
':authority': 'example.com',
2928+
},
2929+
onheaders(headers) {
2930+
console.log('status:', headers[':status']);
2931+
},
2932+
});
2933+
2934+
const decoder = new TextDecoder();
2935+
for await (const chunk of stream) {
2936+
process.stdout.write(decoder.decode(chunk, { stream: true }));
2937+
}
2938+
2939+
await session.close();
2940+
```
2941+
2942+
A few things to note:
2943+
2944+
* `session.createBidirectionalStream({ headers })` automatically
2945+
marks the HEADERS frame as terminal when no `body` is provided —
2946+
the request is `HEADERS` followed by `END_STREAM`.
2947+
* The `onheaders` callback receives the response pseudo-headers and
2948+
regular headers in a single object with lowercase string keys.
2949+
After the callback returns, the same object is also accessible
2950+
via [`stream.headers`][].
2951+
* Reading `for await (const chunk of stream)` consumes the response
2952+
body as `Uint8Array` chunks.
2953+
* HTTP semantic helpers (URL parsing, method/status validation,
2954+
redirects, content negotiation, and so on) are intentionally not
2955+
built in. The caller is responsible for any HTTP-level handling
2956+
beyond the wire framing.
2957+
2958+
### Minimal HTTP/3 server
2959+
2960+
```mjs
2961+
import { listen } from 'node:quic';
2962+
2963+
const encoder = new TextEncoder();
2964+
2965+
const endpoint = await listen((session) => {
2966+
// The session.onstream callback fires for each new client-initiated stream.
2967+
}, {
2968+
sni: { '*': { keys: [defaultKey], certs: [defaultCert] } },
2969+
// ALPN defaults to 'h3'.
2970+
onheaders(headers) {
2971+
// `this` is the QuicStream. Pseudo-headers are available on the
2972+
// request header block (`:method`, `:path`, `:scheme`,
2973+
// `:authority`).
2974+
if (headers[':path'] === '/health') {
2975+
this.sendHeaders({ ':status': '200', 'content-type': 'text/plain' });
2976+
const w = this.writer;
2977+
w.writeSync(encoder.encode('ok\n'));
2978+
w.endSync();
2979+
} else {
2980+
this.sendHeaders({ ':status': '404' }, { terminal: true });
2981+
}
2982+
},
2983+
});
2984+
2985+
console.log('listening on', endpoint.address);
2986+
```
2987+
2988+
Server-side notes:
2989+
2990+
* Setting `onheaders` at the [`listen()`][`quic.listen()`] level
2991+
applies it to every incoming stream (it is wired up before
2992+
`onstream` fires). Setting it inside `onstream` is too late for
2993+
HTTP/3, where the request HEADERS frame is the first thing that
2994+
arrives on the stream.
2995+
* `this.sendHeaders(headers, { terminal: true })` marks the
2996+
response HEADERS frame as terminal (no body follows).
2997+
* For body responses, send headers first, then write to
2998+
`this.writer` and call `endSync()` to send the body and close the
2999+
stream cleanly.
3000+
3001+
### What is not implemented
3002+
3003+
* **Server push**`PUSH_PROMISE` and the related push-stream
3004+
machinery are not implemented and are not on the near-term
3005+
roadmap. Server push has limited deployment in practice, and most
3006+
use cases are better served by Early Hints (`103`) or by direct
3007+
fetches from the client.
3008+
* **WebTransport / extended-CONNECT helpers** — the
3009+
`SETTINGS_ENABLE_CONNECT_PROTOCOL` setting can be negotiated but
3010+
there is no built-in support for the `:protocol` pseudo-header,
3011+
WebTransport datagram demultiplexing, or capsule framing.
3012+
* **Higher-level HTTP semantics** — there is no built-in
3013+
request/response router, URL parsing, content-encoding
3014+
negotiation, body-type coercion, redirect following, or
3015+
cookie handling. These are deliberately left to higher-level
3016+
libraries built on top of `node:quic`.
3017+
28653018
## Performance measurement
28663019

28673020
<!-- YAML
@@ -3343,6 +3496,9 @@ throughput issues caused by flow control.
33433496
[`PerformanceEntry`]: perf_hooks.md#class-performanceentry
33443497
[`PerformanceObserver`]: perf_hooks.md#class-performanceobserver
33453498
[`QuicError`]: #class-quicerror
3499+
[`application.enableConnectProtocol`]: #sessionoptionsapplication
3500+
[`application.enableDatagrams`]: #sessionoptionsapplication
3501+
[`application.qpackMaxDTableCapacity`]: #sessionoptionsapplication
33463502
[`endpoint.maxConnectionsPerHost`]: #endpointmaxconnectionsperhost
33473503
[`endpoint.maxConnectionsTotal`]: #endpointmaxconnectionstotal
33483504
[`error.errorCode`]: #errorerrorcode
@@ -3352,18 +3508,26 @@ throughput issues caused by flow control.
33523508
[`session.close()`]: #sessioncloseoptions
33533509
[`session.destroy()`]: #sessiondestroyerror-options
33543510
[`session.maxPendingDatagrams`]: #sessionmaxpendingdatagrams
3511+
[`session.ondatagram`]: #sessionondatagram
33553512
[`session.onerror`]: #sessiononerror
3513+
[`session.ongoaway`]: #sessionongoaway
33563514
[`session.onkeylog`]: #sessiononkeylog
33573515
[`session.onnewtoken`]: #sessiononnewtoken
3516+
[`session.onorigin`]: #sessiononorigin
33583517
[`session.onqlog`]: #sessiononqlog
3518+
[`session.sendDatagram()`]: #sessionsenddatagramdatagram-encoding
33593519
[`sessionOptions.datagramDropPolicy`]: #sessionoptionsdatagramdroppolicy
33603520
[`sessionOptions.keylog`]: #sessionoptionskeylog
33613521
[`sessionOptions.qlog`]: #sessionoptionsqlog
33623522
[`sessionOptions.sni`]: #sessionoptionssni-server-only
33633523
[`stream.destroy()`]: #streamdestroyerror-options
3524+
[`stream.headers`]: #streamheaders
33643525
[`stream.onerror`]: #streamonerror
33653526
[`stream.onwanttrailers`]: #streamonwanttrailers
33663527
[`stream.pendingTrailers`]: #streampendingtrailers
3528+
[`stream.priority`]: #streampriority
3529+
[`stream.sendHeaders()`]: #streamsendheadersheaders-options
3530+
[`stream.sendInformationalHeaders()`]: #streamsendinformationalheadersheaders
33673531
[`stream.sendTrailers()`]: #streamsendtrailersheaders
33683532
[`stream.setBody()`]: #streamsetbodybody
33693533
[`stream.setPriority()`]: #streamsetpriorityoptions

0 commit comments

Comments
 (0)