quico 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -155,7 +155,7 @@ stack trace ends in code you can open, not in a `.node` binary.
155
155
  |---|---|
156
156
  | **Node.js 18+** | ESM only (`import`). Tested on 18, 20 and 22. |
157
157
  | **No compiler** | Pure JavaScript — no native addons. |
158
- | **Dependencies** | [`lemon-tls`](https://github.com/colocohen/lemon-tls) (TLS 1.3) and [`flat-ranges`](https://www.npmjs.com/package/flat-ranges) (range arithmetic). Nothing else. |
158
+ | **Dependencies** | [`lemon-tls`](https://github.com/colocohen/lemon-tls) (TLS 1.3), [`flat-ranges`](https://www.npmjs.com/package/flat-ranges) (range arithmetic), and `cryptographic-formats` (certificate formats — the dates WebTransport's `serverCertificateHashes` checks; lemon-tls reads certificates with it too). Nothing else. |
159
159
 
160
160
  ```bash
161
161
  npm install quico
@@ -288,12 +288,15 @@ QUICO takes from Node is `node:net` (TCP), `node:dgram` (UDP) and `node:crypto`
288
288
  - Bidirectional and unidirectional streams (Node.js Duplex API)
289
289
  - Unreliable datagrams
290
290
  - Natural API: `req.on('stream')`, `res.sendDatagram()`, `res.writeHead(200)` to accept
291
+ - Over HTTP/3 only: a session asked for over HTTP/1.1 or HTTP/2 is answered `501`,
292
+ with no body, before the application sees it
291
293
 
292
294
  ### WebSocket
293
295
  - Server over all three versions: HTTP/1.1 Upgrade, HTTP/2 extended CONNECT
294
296
  (RFC 8441) and HTTP/3 extended CONNECT (RFC 9220) — one `'upgrade'` event for all
295
- - `WebSocketServer` in the shape of the `ws` package, and a WHATWG-style
296
- `WebSocket` client (HTTP/1.1)
297
+ - `WebSocketServer` in the shape of the `ws` package, and the browser's
298
+ `WebSocket` client (HTTP/1.1), checked against the WPT `websockets/` tests
299
+ - `WebSocketStream` (experimental): the streams version, with backpressure both ways
297
300
 
298
301
  ### CONNECT-UDP
299
302
  - A UDP proxy (RFC 9298, MASQUE) over HTTP/3 — QUIC datagrams — and HTTP/2 and
@@ -1006,6 +1009,7 @@ Creates a server for HTTP/3, HTTP/2 and HTTP/1.1. Returns a server object with
1006
1009
  | `handshakeLoadThreshold` | `number` | `500` | HTTP/3 handshakes under way at which `'handshakeLoad'` fires (`high: true`), and again under half of it (`high: false`). `0`: off. |
1007
1010
  | `maxHandshakesPerAddress` | `number` | `64` | HTTP/3 handshakes under way from one client address; past it an Initial from there makes no connection. `0`: no cap. |
1008
1011
  | `resetTokenSecret` | `Buffer` (≥ 16 bytes) | random | Secret for QUIC stateless-reset tokens. Keep it stable across restarts so clients of a crashed instance get a reset instead of a timeout. |
1012
+ | `tokenSecret` | `Buffer` (≥ 16 bytes) | random | Secret the HTTP/3 address-validation tokens (Retry, and NEW_TOKEN for a client's next connection) are sealed under — node:quic's name. Servers that share it open each other's tokens: several on one port (a thread each, `SO_REUSEPORT`), or one restarted. |
1009
1013
  | `socket` | `dgram.Socket` | `null` | Use an externally-bound IPv4 UDP socket instead of creating one. Must already be bound. |
1010
1014
  | `socket6` | `dgram.Socket` | `null` | Same, for IPv6. |
1011
1015
  | `greaseQuicBit` | `boolean` | `true`; `false` with `socket` / `socket6` | QUIC: invite clients to send the fixed bit unpredictable (RFC 9287), so the bit stays free to change. Off by default on an external socket, whose port may be shared with STUN, TURN or DTLS — told apart from QUIC by that bit (RFC 9443). `true` when that port carries QUIC alone. |
@@ -1087,22 +1091,54 @@ HTTP/1.1 and h2c.
1087
1091
  - `server.listen(port[, host][, callback])` — start listening. Binds UDP (unless
1088
1092
  `http3: false` or external sockets were given) and TCP (unless both fallbacks are
1089
1093
  disabled). Port `0` picks a free port.
1090
- - `server.close([callback])` — stop listening and close all connections.
1094
+ - `server.close([callback])` — as `node:http`'s: no new connections, the idle ones
1095
+ closed at once, and each with a request in flight closed once it is answered — HTTP/2
1096
+ and HTTP/3 with `GOAWAY`, HTTP/1.1 at the end of its response. Then `'close'` and the
1097
+ callback. A second `close()`: the callback gets `ERR_SERVER_NOT_RUNNING`.
1098
+ - `server.closeIdleConnections()` / `server.closeAllConnections()` — as `node:http`'s,
1099
+ on all three versions: the connections with no request in flight (HTTP/2 and HTTP/3
1100
+ are sent `GOAWAY` first), or every one. An HTTP/1.1 socket handed to `'upgrade'` is not
1101
+ the server's any more and stays, as in Node; over HTTP/2 and HTTP/3 a WebSocket or
1102
+ WebTransport session lives on the connection, and closes with it.
1103
+ - `server.getConnections(cb)`, `server.listening`, `server.ref()` / `server.unref()`,
1104
+ `await using server = …` (`Symbol.asyncDispose`) — as `node:net`'s; the count is TCP
1105
+ and QUIC connections.
1106
+ - `server.headersTimeout`, `server.requestTimeout`, `server.maxRequestsPerSocket` (all
1107
+ versions), `server.keepAliveTimeout`, `server.maxHeadersCount` (HTTP/1.1) — readable
1108
+ and settable while the server runs, from the next request or connection on.
1109
+ - The server is an `EventEmitter`, as `node:http`'s: every method (`listeners`,
1110
+ `removeAllListeners`, `prependListener`, …), and `instanceof EventEmitter`.
1111
+ - `server.setSecureContext({ key, cert[, ca, passphrase] })` — as `node:tls`'s: the
1112
+ certificate from the next handshake on, over HTTP/1.1, HTTP/2 and HTTP/3 alike;
1113
+ connections already open keep theirs. How a renewed certificate (Let's Encrypt) is taken
1114
+ without a restart. A key that does not match its certificate, or a missing one, throws —
1115
+ and the certificate in use stays.
1116
+ - `server.addContext(hostname, context)` — as `node:tls`'s: a certificate for one name, by
1117
+ the client's SNI (`'*.example.com'` covers one label; the latest added wins), on all
1118
+ three versions. `context`: `{ key, cert }` or `quico.createSecureContext()`. With an
1119
+ `SNICallback` given to `createServer`, it decides instead (as in Node); when it answers
1120
+ with nothing, the `setSecureContext()` certificate is used.
1091
1121
  - `server.address()`
1092
1122
  - `server.handleConnection(socket)` — serve a TCP socket accepted elsewhere.
1093
1123
  - `server.setTimeout([msecs][, callback])` / `server.timeout` — as in `node:http`:
1094
1124
  a connection idle for `msecs` emits `'timeout'` with its socket, and is destroyed
1095
1125
  if nothing listens. `0` (default) disables it; changes apply to new connections.
1096
1126
  TCP connections (HTTP/1.1, HTTP/2) — an HTTP/3 connection has QUIC's idle timeout.
1097
- - `server.handlePacket(msg, rinfo)` / `server.hasConnection(rinfo)` — feed UDP
1098
- datagrams yourself when sharing a port (external socket mode).
1127
+ - `server.handlePacket(msg, rinfo)` / `server.hasConnection(rinfo[, msg])` — feed UDP
1128
+ datagrams yourself when sharing a port (external socket mode). `hasConnection(rinfo)`
1129
+ asks by the sender's address; `hasConnection(rinfo, msg)` by the connection ID the
1130
+ datagram is addressed to — for servers sharing a port across threads, where a client
1131
+ whose address changed can reach one that does not hold its connection. Ask it before
1132
+ `handlePacket`: given a short-header packet for an ID it does not know, `handlePacket`
1133
+ answers with a Stateless Reset, which would end a connection another server holds.
1099
1134
  - `server.addProtocol(name, handler, { sniff? })` — serve another protocol on the same
1100
1135
  TCP port: by ALPN `name`, or for TLS connections without ALPN, by
1101
1136
  `sniff(firstBytes) → true | false | null`. `handler(socket, protocol)`.
1102
1137
  - `server.getTicketKeys()` / `setTicketKeys(keys)`,
1103
1138
  `getEchKeys()` / `setEchKeys(ech)`,
1104
1139
  `getValidateAddress()` / `setValidateAddress(on)`,
1105
- `getResetTokenSecret()` / `setResetTokenSecret(secret)` — change these at runtime.
1140
+ `getResetTokenSecret()` / `setResetTokenSecret(secret)`,
1141
+ `getTokenSecret()` / `setTokenSecret(secret)` — change these at runtime.
1106
1142
  - `server.setOptions(options)` / `server.getOptions()` — every other option that can
1107
1143
  change while the server runs, in one call, by the names `createServer` takes. See
1108
1144
  [Changing options while running](#changing-options-while-running).
@@ -1513,7 +1549,7 @@ Call `agent.destroy()` to close everything it pools.
1513
1549
 
1514
1550
  ### `quico.createSecureContext(options)`
1515
1551
 
1516
- Creates a TLS context for use with `SNICallback`. Accepts `key`, `cert` and `ca`.
1552
+ Creates a TLS context for `SNICallback`, `server.setSecureContext()` and `server.addContext()`. Accepts `key`, `cert`, `ca` and `passphrase`.
1517
1553
  Re-exported from LemonTLS.
1518
1554
 
1519
1555
  ---
@@ -1727,7 +1763,15 @@ quico.createServer({ key: KEY, cert: CERT }, app).listen(4433);
1727
1763
  // → { "protocol": "3.0" }
1728
1764
  ```
1729
1765
 
1730
- Express, Fastify and Koa are all supported this way.
1766
+ Express (4 and 5), Fastify (`serverFactory`), Koa and socket.io (`new Server(server)`,
1767
+ polling and WebSocket) run on it unchanged, over all three versions — checked against
1768
+ their npm releases.
1769
+
1770
+ `req` and `res` are `node:http`'s, down to the details frameworks rely on:
1771
+ `res.writeHead(status, [name, value, …])` and `[[name, value], …]`, `appendHeader`,
1772
+ `setHeaders(Headers | Map)`, `getRawHeaderNames`, `getHeaders()` (no prototype),
1773
+ `sendDate`, `strictContentLength`, `writeProcessing()` (102), `end(cb)` after the
1774
+ `'finish'` listeners; `req.headersDistinct`, `req.trailersDistinct`, `req.upgrade`.
1731
1775
 
1732
1776
  Multi-domain setups use `SNICallback`, exactly as in `node:https`:
1733
1777
 
@@ -1780,6 +1824,21 @@ quico.createServer({ key: KEY, cert: CERT }, (req, res) => {
1780
1824
  }).listen(4433);
1781
1825
  ```
1782
1826
 
1827
+ **Only over HTTP/3.** A WebTransport session is an extended CONNECT over HTTP/3 (RFC 9220).
1828
+ When one is asked for over HTTP/1.1 or HTTP/2, the server answers it itself: **`501` with no
1829
+ body** (`content-length: 0`, `cache-control: no-store`). The request never reaches the handler,
1830
+ `'request'` or `'upgrade'`. Two signs mark such a request, and either one is enough:
1831
+
1832
+ - the browser's Fetch destination, `Sec-Fetch-Dest: webtransport`;
1833
+ - an extended CONNECT for `:protocol: webtransport` over HTTP/2.
1834
+
1835
+ A browser that cannot reach HTTP/3 may still send its session request over TCP. Firefox 158 does
1836
+ this when HTTP/3 has been excluded for an origin: it sends a plain `GET` with
1837
+ `Sec-Fetch-Dest: webtransport` on a new HTTP/1.1 connection. If that request were answered as a
1838
+ page, the body would land in the browser's WebTransport session, and Firefox would abort the whole
1839
+ browser. With the `501`, the session fails cleanly (`wt.ready` rejects in the page), and the page
1840
+ can fall back to WebSocket.
1841
+
1783
1842
  **Datagrams** (`res.sendDatagram(data)`) are never retransmitted, but they are sent under
1784
1843
  the connection's congestion control (RFC 9221 §5.4): they wait for room in a packet and
1785
1844
  go ahead of stream data. While the path cannot keep up, the oldest are dropped — any that
@@ -1789,14 +1848,27 @@ the peer takes no datagrams). Datagrams sent right after accepting the session a
1789
1848
  until the `200` has left: a browser drops datagrams for a session it does not know yet.
1790
1849
 
1791
1850
  `res.datagrams` is the browser's datagrams object too — the client's `wt.datagrams`,
1792
- the same code: `readable`, `writable`, `createWritable({ sendOrder, sendGroup })`,
1793
- `maxDatagramSize`, `incomingMaxAge`, `outgoingMaxAge` and the high-water marks. Its
1794
- `outgoingMaxAge` is the session's own: sessions on one connection share its queue, each
1795
- with its own age limit and its own count of datagrams dropped.
1851
+ the same code: `readable`, `createWritable({ sendOrder, sendGroup })`, `maxDatagramSize`,
1852
+ `incomingMaxAge`, `outgoingMaxAge`, `incomingMaxBufferedDatagrams` and
1853
+ `outgoingMaxBufferedDatagrams`, as the current W3C spec has them. Each writable has its own
1854
+ queue and backpressure: `writer.ready` waits while `outgoingMaxBufferedDatagrams` of its
1855
+ datagrams wait to go. A write that is not an `ArrayBuffer` or a view onto one, or is
1856
+ empty, rejects with a `TypeError`; one larger than `maxDatagramSize` is dropped. The
1857
+ setters take values as the browser's do: an age of `0` is no limit (`null`), a negative
1858
+ one is a `RangeError`; a buffer count below 1 is 1. Its `outgoingMaxAge` is the session's
1859
+ own: sessions on one connection share its queue, each with its own age limit and its own
1860
+ count of datagrams dropped. The names from before the spec changed still work, for code
1861
+ written then: `datagrams.writable` (one writable, made on first use) and
1862
+ `incomingHighWaterMark` / `outgoingHighWaterMark` (the same settings as
1863
+ `…MaxBufferedDatagrams`).
1796
1864
 
1797
1865
  **Statistics** — `res.getStats()`, as the client's `wt.getStats()` (the same function):
1798
- a Promise of the browser's `WebTransportConnectionStats` — `bytesSent`, `packetsSent`,
1799
- `packetsLost`, `bytesReceived`, `packetsReceived` (whole packets), `smoothedRtt`,
1866
+ a Promise of the browser's `WebTransportConnectionStats` — `bytesSent` (the application's
1867
+ bytes: new stream data and datagram payloads, without framing or retransmissions),
1868
+ `bytesSentOverhead` (what the packets carried besides), `bytesAcknowledged` (the payload the
1869
+ peer acknowledged), `packetsSent`, `packetsLost` and `bytesLost` (packets of stream data or
1870
+ datagrams declared lost, and their size), `bytesReceived`, `packetsReceived` (whole
1871
+ packets), `smoothedRtt`,
1800
1872
  `rttVariation`, `minRtt` (ms), `estimatedSendRate` (bits/s, the congestion control's
1801
1873
  bandwidth estimate; `null` until measured), `atSendCapacity` (the congestion window is
1802
1874
  full) — with QUICO's `congestionWindow` and `bytesInFlight` besides, and `datagrams`:
@@ -1806,6 +1878,13 @@ that has to choose what to drop for a slow receiver reads `atSendCapacity`, or
1806
1878
  `bytesInFlight` against `congestionWindow`: the congestion is the connection's, not one
1807
1879
  stream's.
1808
1880
 
1881
+ **Keying material** — `res.exportKeyingMaterial(label, context, outputLength)`, as the
1882
+ client's `wt.exportKeyingMaterial()`: a Promise of `outputLength` bytes (1 to 4096) from the
1883
+ TLS exporter, this session's own (draft-ietf-webtrans-http3: the label
1884
+ `EXPORTER-WebTransport`, the session ID, the application's label and context, each at most
1885
+ 255 bytes). Both ends get the same bytes for the same label and context, which lets a token
1886
+ be bound to the session it was sent on.
1887
+
1809
1888
  **Priorities** are the browser's: a stream's `sendOrder` and `sendGroup`
1810
1889
  (`stream.sendGroup = res.createSendGroup()`), and `res.sendDatagram(data, { sendOrder,
1811
1890
  sendGroup })` as the browser's `datagrams.createWritable({ sendOrder, sendGroup })`. The
@@ -1834,7 +1913,7 @@ stream are independent:
1834
1913
  | `'committed'` | Our sending part is "all data committed" (draft-ietf-webtrans-overview §4.3; "Data Recvd", RFC 9000 §3.1): every byte written and the FIN acknowledged by the peer. Once; never for a stream reset first. `'finish'` (and `end()`'s callback) only says the bytes were handed to QUIC — a delivery deadline (MOQT's SUBGROUP_DELIVERY_TIMEOUT) runs from the FIN to here, and `resetStream()` before it drops what is left. `sendState` is then `'committed'`. |
1835
1914
  | `stream.id`, `stream.sessionId` | Its QUIC stream ID, and its session's — read-only. |
1836
1915
  | `stream.sendState`, `stream.recvState` | Each direction, read-only: `'open'`, `'finished'` (the FIN sent / received), `'committed'` (ours: all of it and the FIN acknowledged), `'reset'` (we gave up sending / the peer did), `'stopped'` (the peer asked us to stop / we stopped reading); `null` for the direction a unidirectional stream does not have. A `'drain'` says the writer may write; `sendState` says whether what it writes still goes anywhere. |
1837
- | `'close'` | Once both directions are over — by FIN, reset or stop, from either side, in any order. When the peer's reset or stop is what ends the last one, an `Error` (`ERR_WEBTRANSPORT_STREAM_RESET` / `ERR_WEBTRANSPORT_STOP_SENDING`) goes to an `'error'` listener — only if there is one. |
1916
+ | `'close'` | Once both directions are over — by FIN, reset or stop, from either side, in any order. When the peer's reset or stop is what ends the last one, an `Error` (`ERR_WEBTRANSPORT_STREAM_RESET` / `ERR_WEBTRANSPORT_STOP_SENDING`) goes to an `'error'` listener — only if there is one. `pipe()` adds a listener of its own that re-throws, as it does for a `net.Socket`: a stream piped with `pipe()` needs an `'error'` listener of yours, or use `stream.pipeline()`. The session's end is not such a cancel: when the peer closes the session it resets the streams with `WT_SESSION_GONE`, and they close without an `'error'`. |
1838
1917
 
1839
1918
  **Opening streams** is the browser's: `res.createBidirectionalStream(options[, callback])`
1840
1919
  and `res.createUnidirectionalStream(options[, callback])` take `{ sendOrder, sendGroup,
@@ -1902,19 +1981,20 @@ const uni = await wt.createUnidirectionalStream(); // a WritableStre
1902
1981
  await uni.getWriter().abort(new WebTransportError({ streamErrorCode: 7 })); // RESET_STREAM, code 7
1903
1982
 
1904
1983
  const incoming = wt.incomingUnidirectionalStreams.getReader(); // ReadableStream of ReadableStreams
1905
- const dgWriter = wt.datagrams.writable.getWriter(); // and wt.datagrams.readable
1984
+ const dgWriter = wt.datagrams.createWritable().getWriter(); // and wt.datagrams.readable
1906
1985
 
1907
1986
  wt.close({ closeCode: 0, reason: 'done' });
1908
1987
  const { closeCode, reason } = await wt.closed; // the server's, if it closed first
1909
1988
  ```
1910
1989
 
1911
- The whole browser API is there: `ready`, `closed` (resolving with `{ closeCode, reason }`),
1990
+ The whole browser API is there: `ready`, `closed` (see [How the session ends](#how-the-session-ends) below),
1912
1991
  `draining`, `close()`, `createBidirectionalStream()` / `createUnidirectionalStream()` with
1913
1992
  `sendOrder`, `sendGroup` and `waitUntilAvailable`, `incomingBidirectionalStreams`,
1914
- `incomingUnidirectionalStreams`, `datagrams` (`readable`, `writable`, `createWritable()`,
1915
- `maxDatagramSize`, `incomingMaxAge`, `outgoingMaxAge`, `incomingHighWaterMark`,
1916
- `outgoingHighWaterMark`), `createSendGroup()`, `getStats()`, `reliability`,
1917
- `congestionControl`, `protocol`, and stream `getStats()`; the options
1993
+ `incomingUnidirectionalStreams`, `datagrams` (`readable`, `createWritable()`,
1994
+ `maxDatagramSize`, `incomingMaxAge`, `outgoingMaxAge`, `incomingMaxBufferedDatagrams`,
1995
+ `outgoingMaxBufferedDatagrams` — see the server's `res.datagrams` above), `createSendGroup()`, `getStats()`, `reliability`,
1996
+ `congestionControl`, `protocol`, `responseHeaders`, `exportKeyingMaterial()`,
1997
+ `WebTransport.supportsReliableOnly` (`false`: HTTP/3 only), and stream `getStats()`; the options
1918
1998
  `serverCertificateHashes` (the leaf's SHA-256, a certificate valid for at most 14 days, as
1919
1999
  the browser requires), `protocols`, `requireUnreliable`, `congestionControl`,
1920
2000
  `transportParams` (QUICO's own: the windows and stream limits, as the server's),
@@ -1923,11 +2003,50 @@ the browser requires), `protocols`, `requireUnreliable`, `congestionControl`,
1923
2003
  `session` (a TLS session from an earlier `'session'` event, to resume — not with
1924
2004
  `serverCertificateHashes`: a resumed handshake has no certificate to pin),
1925
2005
  `initialCongestionWindow` (as the server's),
1926
- `allowPooling` and the `anticipatedConcurrent…` hints. `WebTransportError` takes the browser's
1927
- `{ streamErrorCode }` form as well as the spec's `(message, options)`. A test runs the same
2006
+ `allowPooling`, the `anticipatedConcurrent…` hints and `datagramsReadableType`. A test runs the same
1928
2007
  client function in Chromium and in Node against a QUICO server and checks both see the same.
1929
2008
  `sendOrder` and `sendGroup` order what the client sends, as on the server (see above).
1930
2009
 
2010
+ **The browser's interfaces** are exported, for `instanceof` as in the browser:
2011
+ `WebTransportBidirectionalStream`, `WebTransportSendStream`, `WebTransportReceiveStream`,
2012
+ `WebTransportWriter`, `WebTransportSendGroup`, `WebTransportDatagramsWritable`,
2013
+ `WebTransportDatagramDuplexStream`. As in the browser, none of them can be constructed by
2014
+ the application. `WebTransportError` is a `DOMException` (its `name` is
2015
+ `'WebTransportError'`, with `source` and `streamErrorCode`) and takes the browser's
2016
+ `{ message, source, streamErrorCode }` form as well as the spec's `(message, options)`. Where
2017
+ QUICO gives a Node code too (`ERR_WEBTRANSPORT_SESSION_LOST`, say), it is in `err.code`, in
2018
+ place of the `DOMException`'s number.
2019
+
2020
+ | | |
2021
+ |---|---|
2022
+ | `wt.responseHeaders` | The server's answer to the CONNECT: a `Headers` that cannot be changed, `null` until `ready`. No pseudo-headers and no `wt-protocol` (that is `wt.protocol`). `set-cookie` is there (`getSetCookie()`): a browser keeps it from its page, but in Node the program is the user agent, as with Node's `fetch`. |
2023
+ | `writer.atomicWrite(chunk)` | `getWriter()` of a send stream gives a `WebTransportWriter`. An atomic write goes only if all of it fits the flow-control window when its turn comes; otherwise none of it is sent and it rejects with an `AbortError`, as does any atomic write queued behind it, and the stream goes on. A plain write queued behind it fails the stream, as the spec has it. |
2024
+ | `writer.commit()` | What was written so far is delivered even if the stream is aborted later: `abort()` then sends RESET_STREAM_AT with that many bytes reliable (a MOQT subgroup's header, say) — `stream.resetStream(code, { reliableSize })` in the Node API. A peer that does not take RESET_STREAM_AT gets a plain RESET_STREAM. |
2025
+ | `readable.getReader({ mode: 'byob' })` | Streams are byte streams, as the browser's: a BYOB reader reads into its own buffer, and what does not fit stays for the next read. Datagrams too, with `datagramsReadableType: 'bytes'`: each datagram whole into the reader's buffer, a `RangeError` if it does not fit. |
2026
+
2027
+ It behaves as the browser's does where the web-platform-tests check it:
2028
+
2029
+ - **The constructor checks its arguments.** A URL that does not parse, is not `https:`, or
2030
+ has a fragment is a `SyntaxError` (a `DOMException`, as Node's own `WebSocket` throws), and
2031
+ so is a protocol in `protocols` that is empty, longer than 512, not printable ASCII, or
2032
+ given twice. A malformed `serverCertificateHashes` entry, a `congestionControl` that is
2033
+ not one of the three, or an `anticipatedConcurrent…` hint outside 0–65535 is a
2034
+ `TypeError`. The hints are attributes that can be set later, with the same checks.
2035
+ - **Streams and datagrams can be used before `ready`.** `create…Stream()` waits for the
2036
+ session and then resolves; a datagram written meanwhile goes once the session is up.
2037
+ - **The Promises reject as the browser's do.** `create…Stream()` on a session that is over
2038
+ rejects with an `InvalidStateError`, a send group of another session's with a
2039
+ `TypeError`, and a stream past the server's limit without `waitUntilAvailable` with a
2040
+ `QuotaExceededError`. `getStats()` on a session that failed rejects with an
2041
+ `InvalidStateError`.
2042
+ - **A stream fails as soon as its direction is given up on.** The server's `STOP_SENDING`
2043
+ rejects `writer.closed`, its `RESET_STREAM` rejects `reader.closed`, and the session's
2044
+ end fails both, without waiting for the next read or write. A stream write that is not
2045
+ an `ArrayBuffer` or a view onto one, or is empty, rejects with a `TypeError`.
2046
+ - **The anticipated-streams hints take effect.** Given to the constructor, they raise the
2047
+ stream limits the client grants the server from the start, unless `transportParams`
2048
+ sets those limits itself.
2049
+
1931
2050
  The same object also has a Node-style API, for Node code:
1932
2051
 
1933
2052
  | | |
@@ -1953,7 +2072,45 @@ platform roots unless `ca` is given or `serverCertificateHashes` pins it. If the
1953
2072
  cannot be established, `ready` and `closed` reject with a `WebTransportError` (source
1954
2073
  `'session'`, keeping node:tls's `err.code`, e.g. `DEPTH_ZERO_SELF_SIGNED_CERT`, when TLS is
1955
2074
  the reason), `state` becomes `'failed'` and the connection is closed, as in the browser.
1956
- `'error'` is emitted too, but only when a listener is attached.
2075
+ `'error'` is emitted too, but only when a listener is attached. Any `2xx` answer to the
2076
+ CONNECT establishes the session, as in the browser (draft-ietf-webtrans-http3 §3.3), not
2077
+ only `200`.
2078
+
2079
+ **Fields on the CONNECT request.** A browser always sends its page's `Origin`, and servers
2080
+ check it (the web-platform-tests WebTransport server refuses a session without one). Node has
2081
+ no page, so the client takes `origin`, and `headers` for other fields, as QUICO's WebSocket
2082
+ client does:
2083
+
2084
+ ```js
2085
+ new WebTransport(url, { origin: 'https://app.example', headers: { authorization: 'Bearer …' } });
2086
+ ```
2087
+
2088
+ `headers` is the browser's `HeadersInit`: a `Headers`, a list of `[name, value]` pairs, or
2089
+ an object. The fields go after the pseudo-headers and are treated as the browser's
2090
+ `Headers` treats them: names are lower-cased, leading and trailing whitespace is removed
2091
+ from values, and a name given twice is sent once with its values joined by `, `.
2092
+
2093
+ | Field | What happens |
2094
+ |---|---|
2095
+ | A pseudo-header, `wt-available-protocols` (that is `protocols`), a name or value that is not valid | The constructor throws a `TypeError`, as the browser's does |
2096
+ | `host`, `te`, `content-length`, `trailer`, connection-specific fields (`connection`, `keep-alive`, `transfer-encoding`, `upgrade`, …) | Dropped, as the browser drops them: they cannot go on an HTTP/3 request (the authority is `:authority`) |
2097
+ | `cookie`, `referer`, `sec-*` and the rest | Sent. A browser keeps them from a page for the page's safety; in Node the program is the user agent, as with Node's `fetch` |
2098
+
2099
+ ##### How the session ends
2100
+
2101
+ | | `closed` | `'close'` |
2102
+ |---|---|---|
2103
+ | Either side sends `CLOSE_WEBTRANSPORT_SESSION` (`wt.close({ closeCode, reason })`, `res.closeSession(code, reason)`) | resolves with `{ closeCode, reason }` | emitted |
2104
+ | The server ends the CONNECT stream with a FIN and no capsule | resolves with `{ closeCode: 0, reason: '' }`, at once (draft-ietf-webtrans-http3 §5) | emitted |
2105
+ | The session is lost: the server resets the CONNECT stream, or the connection goes (an error, the idle timeout, the server gone) | **rejects** with a `WebTransportError` (source `'session'`, `code` `'ERR_WEBTRANSPORT_SESSION_LOST'`, and `quicError` `{ code, application, reason }` when the peer closed the connection), as soon as the server's CONNECTION_CLOSE arrives, not after QUIC's draining period; `state` is `'failed'` | emitted |
2106
+ | The session could not be established, or `wt.close()` was called before `ready` | rejects, with the same error as `ready` | emitted |
2107
+
2108
+ As in the browser, `{ closeCode: 0 }` means a close someone asked for; a session that was
2109
+ lost rejects instead. The session's streams, its incoming-stream queues and its datagrams
2110
+ fail with it. When the session was lost or never established, they fail with the very
2111
+ error object `closed` rejects with. After our own `close()` they fail with an
2112
+ `AbortError`, and after the server's with a `WebTransportError`. A clean close closes the
2113
+ incoming-stream queues and the datagrams' `readable` instead of failing them.
1957
2114
 
1958
2115
  #### Client (Browser)
1959
2116
 
@@ -1964,7 +2121,7 @@ needed:
1964
2121
  const wt = new WebTransport('https://yourserver.com:4433/live');
1965
2122
  await wt.ready;
1966
2123
 
1967
- const writer = wt.datagrams.writable.getWriter();
2124
+ const writer = wt.datagrams.createWritable().getWriter();
1968
2125
  await writer.write(new TextEncoder().encode('Hello QUIC'));
1969
2126
 
1970
2127
  const reader = wt.datagrams.readable.getReader();
@@ -2026,7 +2183,58 @@ The client connects over HTTP/1.1. Options beyond the WHATWG constructor: `origi
2026
2183
  `headers`, `ca`, `rejectUnauthorized`, `maxPayload`, `agent` (whose TLS sessions a
2027
2184
  `wss://` connection resumes with and keeps — `globalAgent` by default) and `session` (one
2028
2185
  to resume with, from an earlier `'session'`). `ws.on('session', buf)`: each one the server
2029
- sends.
2186
+ sends. The second argument may also be the browser's options bag: `new WebSocket(url, { protocols: ['chat'] })`.
2187
+
2188
+ It behaves as a browser's does (WHATWG, WPT `websockets/`):
2189
+
2190
+ - Messages go in the order `send()` was called — a `Blob` included, which is read first.
2191
+ - `close(code, reason)` checks as the browser: `null` is 0 and a code is clamped to 16 bits, so
2192
+ anything but 1000 or 3000–4999 is an `InvalidAccessError`; a reason over 123 bytes, a
2193
+ `SyntaxError`. `close()` while connecting abandons the handshake: `'error'`, then `'close'`
2194
+ 1006, both on a later turn.
2195
+ - A URL with a fragment (even an empty `#`), or protocols repeated in another case
2196
+ (`['chat', 'CHAT']`): `SyntaxError`.
2197
+ - A port browsers refuse (Fetch's bad ports: 22, 25, 6000, …): never connected — `'error'`,
2198
+ then `'close'` 1006.
2199
+ - User and password in the URL (`wss://user:pass@host/`): sent as `Authorization: Basic`,
2200
+ unless `headers` has one.
2201
+ - `MessageEvent.origin` is the URL's origin; `send()` after close adds to `bufferedAmount`.
2202
+
2203
+ #### WebSocketStream (experimental)
2204
+
2205
+ The browser's streams-based WebSocket ([explainer](https://github.com/ricea/websocketstream-explainer);
2206
+ Chrome ships it, not yet a standard — the API may still change). The same connection, but a
2207
+ `ReadableStream` and a `WritableStream` in place of events, so both directions have backpressure:
2208
+
2209
+ ```js
2210
+ import { WebSocketStream, WebSocketError } from 'quico';
2211
+
2212
+ const wss = new WebSocketStream('wss://localhost:4433/chat',
2213
+ { protocols: ['chat'], signal: AbortSignal.timeout(5000), rejectUnauthorized: false });
2214
+ const { readable, writable, protocol } = await wss.opened;
2215
+
2216
+ const writer = writable.getWriter();
2217
+ await writer.write('hello'); // resolves once the frame is sent
2218
+ for await (const message of readable) { // a string, or a Uint8Array
2219
+ console.log(message);
2220
+ }
2221
+ const { closeCode, reason } = await wss.closed;
2222
+ ```
2223
+
2224
+ - **Receiving.** A message nobody has read holds the socket: it is paused, TCP fills up, and the
2225
+ server's sends wait. With `WebSocket`, every message arrives as an event, however fast, into memory.
2226
+ - **Sending.** `write()` resolves when the frame has gone to the socket — a writer that awaits it
2227
+ goes as fast as the connection, with nothing piling up. `WebSocket`'s `send()` returns at once.
2228
+ - `close({ closeCode, reason })` — the checks of `WebSocket.close()`; a reason with no code is 1000.
2229
+ `closed` resolves with the code and reason the peer closed with.
2230
+ - A connection lost, or a close that left writes unsent: `closed` rejects with a `WebSocketError`
2231
+ (a `DOMException` with `closeCode` — 1006 when there was no closing handshake — and `reason`),
2232
+ and the readable and writable error with it.
2233
+ - `readable.cancel(err)` / `writable.abort(err)` close with the code and reason of a `WebSocketError`
2234
+ given: `new WebSocketError('', { closeCode: 4000, reason: 'bye' })`.
2235
+ - `signal` aborted before the connection is open: it is abandoned, `opened` and `closed` reject with
2236
+ the signal's reason; once open, it does nothing.
2237
+ - Options besides `protocols` and `signal` are `WebSocket`'s (`origin`, `headers`, `ca`, …).
2030
2238
 
2031
2239
  ---
2032
2240
 
@@ -2250,10 +2458,11 @@ For non-HTTP protocols over QUIC, or full control of the connection.
2250
2458
  | `handshakeLoadThreshold`, `maxHandshakesPerAddress` | `number` | `500`, `64` | As `createServer`; `getPendingHandshakes()`, `setOptions()` / `getOptions()` too |
2251
2459
  | `pmtuDiscovery`, `maxUdpPayloadSize`, `initialRtt`, `reorderWindow` | | | As `createServer` |
2252
2460
  | `resetTokenSecret` | `Buffer` | random | Stateless-reset secret |
2461
+ | `tokenSecret` | `Buffer` (≥ 16 bytes) | random | Address-validation token secret (Retry, NEW_TOKEN), as `createServer` |
2253
2462
  | `socket` / `socket6` | `dgram.Socket` | `null` | External, already-bound UDP sockets |
2254
2463
 
2255
2464
  Returns `{ listen, close, on, handlePacket, hasConnection }` plus the runtime getters and
2256
- setters for ticket keys, address validation and the reset secret. The `'connection'`
2465
+ setters for ticket keys, address validation, the reset secret and the token secret. The `'connection'`
2257
2466
  event fires with `(quicConnection, peer)`.
2258
2467
 
2259
2468
  ```js
@@ -2274,7 +2483,7 @@ qs.on('connection', (quic, peer) => {
2274
2483
  qs.listen(4433);
2275
2484
  ```
2276
2485
 
2277
- `handlePacket(msg, rinfo)` and `hasConnection(rinfo)` support **external socket
2486
+ `handlePacket(msg, rinfo)` and `hasConnection(rinfo[, msg])` support **external socket
2278
2487
  mode**, where QUICO shares a UDP port with another protocol (STUN/TURN, for example)
2279
2488
  and you route datagrams yourself.
2280
2489
 
@@ -2298,13 +2507,17 @@ and you route datagrams yourself.
2298
2507
  | `quic.earlyDataStatus` | 0-RTT: `null` (none), `'offered'` (a client's, undecided), `'accepted'`, `'rejected'` |
2299
2508
  | `quic.isEarlyStream(id)` | Server: whether the stream was opened by 0-RTT data |
2300
2509
  | `quic.whenConfirmed(cb)` | `cb()` once the handshake is confirmed (RFC 9001 §4.1.2), at once if it is |
2301
- | `quic.alpnProtocol`, `servername`, `getCertificate()`, `getTicketInfo()`, `isSessionReused()` | What the handshake decided, with `TLSSocket`'s names |
2510
+ | `quic.alpnProtocol`, `servername`, `getCertificate()`, `getTicketInfo()`, `isSessionReused()` | What the handshake decided, with `TLSSocket`'s names — `getCertificate()` in its shape too (`subject` and `issuer` as objects, `{ CN, O, … }`), lemon-tls's `certificateObject` |
2302
2511
  | `quic.on(event, fn)` / `off` | Event subscription |
2303
2512
 
2304
2513
  **Events:** `'connect'`, `'stream'` (`id, data, fin`), `'stream_reset'` (`id, code,
2305
2514
  finalSize`), `'stop_sending'` (`id, code`), `'datagram'`, `'path'` (`address, port`),
2306
2515
  `'peer_transport_params'`, `'close'`, `'error'`, `'keylog'`; a client's 0-RTT: `'early'` (it may
2307
2516
  send application data now), `'early_accepted'`, `'early_rejected'` (what went early is forgotten — send it again).
2517
+ `'closing'` (`peerClose` — `{ error, application, reason }` when the peer's CONNECTION_CLOSE
2518
+ ended it, `null` when we did) fires once, the moment the connection is over for the
2519
+ application; `'close'` follows when the closing or draining period ends (RFC 9000 §10.2, up to
2520
+ 3 s), which is packet housekeeping. Code that reacts to the end belongs on `'closing'`.
2308
2521
 
2309
2522
  **Connection options** (when constructing directly): `idleTimeout` (default `30000`),
2310
2523
  `handshakeTimeout` (default `10000`), `keepAlive` (`true` → half the idle timeout, or
@@ -2470,6 +2683,7 @@ quico/
2470
2683
  ├── ws.js — WebSocket framing, and the WHATWG and ws-style event surfaces
2471
2684
  ├── ws_server.js — WebSocketServer, upgradeWebSocket
2472
2685
  ├── ws_client.js — WebSocket client
2686
+ ├── ws_stream.js — WebSocketStream, WebSocketError (experimental)
2473
2687
  │
2474
2688
  ├── ja4h.js — JA4H request fingerprint
2475
2689
  ├── h2_fingerprint.js — HTTP/2 and HTTP/3 fingerprints
@@ -2595,9 +2809,9 @@ with the error the RFC names, not left to grow.
2595
2809
  parameters, packet headers, HTTP/1.1, WebSocket, HPACK and HTTP/2 connections, QPACK,
2596
2810
  capsules — and HTTP/3 and QUIC frames on live connections, with mutated valid input and
2597
2811
  noise; none ends in an exception of its own, a hang, or an internal-error close.
2598
- - **Closing says so**: `server.close()` sends every connection its `CONNECTION_CLOSE`
2599
- and closes the socket only once those have left, so clients learn at once rather than
2600
- by their idle timeout.
2812
+ - **Closing says so**: `server.close()` lets requests in flight finish, then sends each
2813
+ connection its `CONNECTION_CLOSE` and closes the socket only once those have left, so
2814
+ clients learn at once rather than by their idle timeout.
2601
2815
 
2602
2816
  To report a vulnerability, please see [SECURITY.md](./SECURITY.md) — or write to
2603
2817
  **support@quicojs.dev** — rather than opening a public issue.