quico 0.2.0 → 0.4.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
@@ -7,82 +7,157 @@
7
7
  <em>Pure JavaScript implementation of QUIC, HTTP/3, QPACK &amp; WebTransport for Node.js</em>
8
8
  </p>
9
9
 
10
+ <p align="center">
11
+ <strong>The entire HTTP/3 stack — QUIC, TLS 1.3, QPACK, congestion control — in JavaScript you can read.</strong>
12
+ </p>
13
+
10
14
  <p align="center">
11
15
  <a href="https://www.npmjs.com/package/quico">
12
16
  <img src="https://img.shields.io/npm/v/quico?color=blue" alt="npm">
13
17
  </a>
14
- <img src="https://img.shields.io/badge/status-in%20development-yellow" alt="status">
18
+ <img src="https://img.shields.io/badge/status-beta-yellow" alt="status">
19
+ <img src="https://img.shields.io/badge/interop-6%2F6%20implementations-brightgreen" alt="interop">
15
20
  <img src="https://img.shields.io/github/license/colocohen/quico?color=brightgreen" alt="license">
16
21
  </p>
17
22
 
18
23
  ---
19
24
 
20
- > **⚠️ Project status: Active development.**
21
- > APIs may change before v1.0. Use at your own risk and please report issues!
25
+ > **⚠️ Project status: Beta — active development.**
26
+ > The core transport is verified against six independent QUIC implementations
27
+ > (see [Interoperability](#-interoperability)), but APIs may still change before
28
+ > v1.0. Please report issues!
22
29
 
23
30
  ---
24
31
 
25
-
26
32
  ## Table of Contents
33
+
27
34
  1. [What is QUIC / HTTP/3?](#-what-is-quic--http3)
28
35
  2. [Why QUICO?](#-why-quico)
29
- 3. [Quick Start](#-quick-start)
30
- 4. [Features](#-features)
31
- 5. [Node.js API Compatibility](#-nodejs-api-compatibility)
32
- 6. [WebTransport](#-webtransport)
33
- 7. [Testing](#-testing)
34
- 8. [Project Structure](#-project-structure)
35
- 9. [Roadmap](#-roadmap)
36
- 10. [Sponsors](#-sponsors)
37
- 11. [Contact](#-contact)
38
- 12. [License](#-license)
36
+ 3. [Requirements & Installation](#-requirements--installation)
37
+ 4. [Quick Start](#-quick-start)
38
+ 5. [Features](#-features)
39
+ 6. [Design Choices](#-design-choices)
40
+ - [The send model: range reconciliation](#the-send-model-range-reconciliation-not-retransmission)
41
+ - [Congestion control](#congestion-control-bbr-style-not-loss-based)
42
+ - [Stream scheduling](#stream-scheduling-byte-level-round-robin)
43
+ - [Loss detection](#loss-detection-timers-and-how-we-treat-late-acks)
44
+ - [Flow control](#flow-control-consumption-based-windows)
45
+ 7. [Interoperability](#-interoperability)
46
+ 8. [API Reference](#-api-reference)
47
+ - [`createServer()`](#quicocreateserveroptions-requestlistener)
48
+ - [`request()` / `get()`](#quicorequesturl-options-callback)
49
+ - [`IncomingMessage`](#incomingmessage) · [`ServerResponse`](#serverresponse) · [`ClientRequest`](#clientrequest)
50
+ - [`Agent`](#agent) · [`createSecureContext()`](#quicocreatesecurecontextoptions)
51
+ - [Framework compatibility](#framework-compatibility)
52
+ - [WebTransport](#webtransport)
53
+ - [Low-level QUIC API](#low-level-quic-api)
54
+ 9. [Configuration & Tuning](#-configuration--tuning)
55
+ 10. [Debugging](#-debugging)
56
+ 11. [Testing](#-testing)
57
+ 12. [Project Structure](#-project-structure)
58
+ 13. [Known Limitations](#-known-limitations)
59
+ 14. [Roadmap](#-roadmap)
60
+ 15. [Contributing](#-contributing)
61
+ 16. [Sponsors](#-sponsors)
62
+ 17. [Contact](#-contact)
63
+ 18. [License](#-license)
39
64
 
65
+ ---
40
66
 
41
67
  ## ⚡ What is QUIC / HTTP/3?
42
68
 
43
- **QUIC** is the future of Internet transport protocols. Created at Google and standardized by the IETF, QUIC powers **HTTP/3** and is already deployed at scale by the world’s largest platforms. It was specifically designed to overcome the limitations of TCP and deliver a faster, smoother, and more resilient web.
69
+ **QUIC is the future of Internet transport protocols.** Created at Google,
70
+ standardized by the IETF as [RFC 9000](https://www.rfc-editor.org/rfc/rfc9000), QUIC
71
+ powers **HTTP/3** and is already deployed at scale by the world's largest platforms.
72
+ It was specifically designed to overcome the limitations of TCP and deliver a faster,
73
+ smoother, and more resilient web.
44
74
 
45
75
  Key advantages of QUIC include:
46
76
 
47
- - **Eliminating bottlenecks**: With TCP, data must arrive strictly in order. If a single packet is lost or delayed, all subsequent packets are blocked until it arrives — a phenomenon called **head-of-line blocking**. QUIC removes this bottleneck by running over UDP, so each package arrives on its own and avoids dependence on delayed pieces of data.
48
-
49
- - **UDP efficiency**: By running over UDP, QUIC bypasses decades of kernel-level constraints, enabling lightning-fast performance even on constrained devices such as smartphones, IoT hardware, or edge gateways.
50
-
51
- - **Security by default**: TLS 1.3 is built directly into the protocol. Every connection is encrypted — no exceptions, no downgrade paths.
77
+ - **Eliminating bottlenecks** — With TCP, data must arrive strictly in order. If a
78
+ single packet is lost or delayed, *all* subsequent packets are blocked until it
79
+ arrives — a phenomenon called **head-of-line blocking**. QUIC removes this
80
+ bottleneck by running over UDP and multiplexing independent streams: each stream
81
+ makes progress on its own, and a loss on one never stalls the others.
52
82
 
53
- - **Seamless mobility**: Connections remain stable as devices move across networks (e.g., Wi-Fi → 4G/5G) or switch IP addresses, without breaking the session.
83
+ - **UDP efficiency** — By running over UDP, QUIC bypasses decades of kernel-level TCP
84
+ constraints, enabling fast connection handling even on constrained devices such as
85
+ smartphones, IoT hardware, or edge gateways.
54
86
 
55
- - **Lower latency**: QUIC merges the transport and TLS handshake into fewer round-trips, significantly reducing connection setup time.
87
+ - **Security by default** — TLS 1.3 is built directly into the protocol. Every
88
+ connection is encrypted — no exceptions, no downgrade paths. Even the packet
89
+ headers are cryptographically protected.
56
90
 
57
- - **Smarter congestion control**: Advanced congestion control algorithms (such as BBR) continuously measure bandwidth, round-trip time, and loss in real time. They dynamically adjust to real-world network conditions.
91
+ - **Seamless mobility** — Connections are identified by connection IDs rather than
92
+ IP/port 4-tuples, so a session can survive a device moving across networks
93
+ (Wi-Fi → 4G/5G) or changing addresses.
58
94
 
95
+ - **Lower latency** — QUIC merges the transport and TLS handshakes into a single
96
+ exchange, significantly reducing connection setup time.
59
97
 
60
- QUIC combines **UDP speed, TCP reliability, mandatory TLS security, and adaptive multiplexing** into one powerful transport layer.
98
+ - **Smarter congestion control** — Modern algorithms such as BBR continuously measure
99
+ bandwidth and round-trip time instead of only reacting to loss, adapting to
100
+ real-world network conditions in real time.
61
101
 
62
- **HTTP/3** (h3 in short) is the layer on top of QUIC. This is the evolution of the web’s application layer. Instead of riding over TCP, HTTP/3 maps HTTP requests and responses directly onto QUIC streams.
63
- HTTP/3 is also himself dramatically reduces overhead and improves efficiency - thats bring faster page loads and real-time applications that scale across the modern web.
102
+ QUIC combines **UDP speed, TCP reliability, mandatory TLS security, and adaptive
103
+ multiplexing** into one transport layer.
64
104
 
105
+ **HTTP/3** ([RFC 9114](https://www.rfc-editor.org/rfc/rfc9114)) is the layer on top:
106
+ the evolution of the web's application protocol. Instead of riding over TCP, HTTP/3
107
+ maps requests and responses directly onto QUIC streams, with
108
+ [QPACK](https://www.rfc-editor.org/rfc/rfc9204) header compression designed to avoid
109
+ reintroducing head-of-line blocking. The result: faster page loads and real-time
110
+ applications that scale across the modern web.
65
111
 
112
+ ---
66
113
 
67
114
  ## 🧠 Why QUICO?
68
115
 
69
- Node.js is the backbone of countless modern web applications, but it currently lacks a native QUIC implementation. Supporting QUIC inside Node.js requires **deep access to TLS internals** that Node's existing TLS APIs don't expose. Beyond that, QUIC demands a **highly complex architecture** — intricate state machines, packet schedulers, and congestion control mechanisms.
70
-
71
- **QUICO** brings this missing capability directly into the **Node.js ecosystem**. It is a from-scratch JavaScript implementation of QUIC, HTTP/3 and WebTransport, built without relying on OpenSSL or native code. At its core, it uses [LemonTLS](https://github.com/colocohen/lemon-tls) — a pure JavaScript TLS 1.3 library built by the same author — to provide the cryptographic layer that QUIC requires. Together, QUICO and LemonTLS form a complete, fully auditable JavaScript networking stack from UDP to HTTP/3.
116
+ Node.js is the backbone of countless modern web applications — yet it has **no native
117
+ QUIC implementation**. The browser you're reading this in speaks HTTP/3. The CDN that
118
+ served this page speaks HTTP/3. Your Node server? Stuck on TCP.
119
+
120
+ Why? Supporting QUIC inside Node.js requires **deep access to TLS internals** that
121
+ Node's existing TLS APIs simply don't expose. TLS-over-TCP hands you an encrypted
122
+ *byte stream*; QUIC needs something entirely different — the raw **handshake secrets**
123
+ at each encryption level, so it can derive its own packet-protection keys and encrypt
124
+ each packet individually. `node:tls` was never built to hand those out. Beyond the
125
+ crypto, QUIC demands a genuinely complex architecture: interlocking state machines
126
+ across three packet-number spaces, a packet scheduler with pacing, loss detection
127
+ timers, and congestion control.
128
+
129
+ **QUICO brings this missing capability directly into the Node.js ecosystem.** It is a
130
+ from-scratch JavaScript implementation of QUIC, HTTP/3 and WebTransport, built without
131
+ OpenSSL and without a single line of native code. The cryptographic layer is
132
+ [LemonTLS](https://github.com/colocohen/lemon-tls) — a pure JavaScript TLS 1.3 library
133
+ built by the same author specifically to expose what QUIC needs. Together, QUICO and
134
+ LemonTLS form a complete, fully auditable JavaScript networking stack from the UDP
135
+ socket all the way up to HTTP/3.
136
+
137
+ No build tools. No platform-specific binaries. No node-gyp. It runs anywhere Node runs
138
+ — Linux, macOS, Windows, ARM, Docker, edge — and when something breaks at 3 AM, the
139
+ stack trace ends in code you can open, not in a `.node` binary.
72
140
 
73
- **What this means for you:**
141
+ ---
74
142
 
75
- - **`npm install` and go** — no build tools, no platform-specific binaries, no gyp
76
- - **Works everywhere Node.js runs** — Linux, macOS, Windows, ARM, Docker, edge
77
- - **Debuggable top to bottom** — every packet, every frame, every byte is JavaScript you can step through
143
+ ## 📋 Requirements & Installation
78
144
 
79
-
80
- ## 📦 Quick Start
145
+ | Requirement | Notes |
146
+ |---|---|
147
+ | **Node.js 18+** | ESM only (`import`). Tested on 18, 20 and 22. |
148
+ | **No compiler** | Pure JavaScript — no native addons. |
81
149
 
82
150
  ```bash
83
151
  npm install quico
84
152
  ```
85
153
 
154
+ QUICO is an **ES module**. Use `import`, and make sure your `package.json` has
155
+ `"type": "module"` (or use the `.mjs` extension).
156
+
157
+ ---
158
+
159
+ ## 📦 Quick Start
160
+
86
161
  ### Server
87
162
 
88
163
  ```js
@@ -90,7 +165,7 @@ import quico from 'quico';
90
165
  import fs from 'node:fs';
91
166
 
92
167
  const server = quico.createServer({
93
- key: fs.readFileSync('server.key'),
168
+ key: fs.readFileSync('server.key'),
94
169
  cert: fs.readFileSync('server.crt')
95
170
  }, (req, res) => {
96
171
  res.writeHead(200, { 'content-type': 'text/plain' });
@@ -108,48 +183,521 @@ server.listen(4433, () => {
108
183
  import quico from 'quico';
109
184
 
110
185
  quico.request('https://www.google.com/', (res) => {
111
- console.log('Status:', res.statusCode); // 200
112
- console.log('Protocol:', res.httpVersion); // "3.0"
186
+ console.log('Status:', res.statusCode); // 200
187
+ console.log('Protocol:', res.httpVersion); // "3.0"
113
188
  res.on('data', (chunk) => process.stdout.write(chunk));
114
- });
189
+ }).end();
115
190
  ```
116
191
 
117
- > 📂 For more examples, see [`examples/`](./examples)
192
+ > ⚠️ Don't forget `.end()` — like `node:https`, the request isn't sent until you call it.
193
+
194
+ > 📂 More examples in [`examples/`](./examples)
195
+
196
+ ---
118
197
 
119
198
 
120
199
  ## ✨ Features
121
200
 
122
201
  ### HTTP/3 Server & Client
123
- - Full request/response cycle over QUIC
124
- - QPACK header compression with static and dynamic tables
125
- - Standard Node.js API: `req.headers`, `res.writeHead()`, `res.end()`
126
- - Multiple requests per connection (stream multiplexing)
127
- - Automatic `Alt-Svc` header for HTTP/3 protocol discovery
128
- - H3 → H2 → H1 fallback when QUIC isn't available
129
- - Tested against Google, Cloudflare, Facebook, Microsoft and nginx QUIC servers
202
+ - Full request/response cycle over QUIC (RFC 9114)
203
+ - QPACK header compression, static and dynamic tables (RFC 9204)
204
+ - Node.js-compatible API: `req.headers`, `res.writeHead()`, `res.end()`
205
+ - Stream multiplexing — many concurrent requests on one connection
206
+ - Connection pooling and coalescing: concurrent requests to the same origin share
207
+ a single QUIC connection
208
+ - Automatic `Alt-Svc` advertisement for HTTP/3 discovery
209
+ - Automatic H3 → H2 → H1.1 fallback when QUIC is unavailable
210
+
211
+ ### QUIC Transport
212
+ - QUIC v1 (RFC 9000) — Initial, Handshake and 1-RTT packet spaces
213
+ - TLS 1.3 handshake via LemonTLS, no OpenSSL (RFC 9001)
214
+ - Version Negotiation (RFC 9000 §6)
215
+ - HelloRetryRequest — X25519, P-256, P-384
216
+ - AES-128-GCM and AES-256-GCM
217
+ - Key Update — bidirectional key phase rotation (RFC 9001 §6)
218
+ - Connection ID management and packet coalescing
219
+ - Stream and connection-level flow control with sliding windows
220
+ - Loss detection with PTO and exponential backoff (RFC 9002)
221
+ - BBR-style congestion control with pacing
222
+ - Unreliable datagrams (RFC 9221)
223
+ - Stateless reset, idle timeout, keep-alive
224
+ - TLS keylog export for Wireshark decryption
130
225
 
131
226
  ### WebTransport
132
- - Client and server implementation
133
- - Bidirectional and unidirectional streams (Node.js Duplex + WHATWG Streams API)
134
- - Unreliable datagrams (RFC 9297)
227
+ - Client and server (RFC 9297 + draft-ietf-webtrans-http3)
228
+ - Bidirectional and unidirectional streams (Node.js Duplex API)
229
+ - Unreliable datagrams
135
230
  - Natural API: `req.on('stream')`, `res.sendDatagram()`, `res.writeHead(200)` to accept
136
231
 
137
- ### QUIC Transport
138
- - Full TLS 1.3 handshake via LemonTLS (no OpenSSL)
139
- - Initial, Handshake, and 1-RTT packet handling
140
- - Connection ID management
141
- - ACK processing and flow control
142
- - Key updates (key phase bit rotation)
143
- - HelloRetryRequest support (P-256, P-384, X25519)
144
- - AES-128-GCM and AES-256-GCM cipher suites
145
- - Handshake timeout
232
+ ---
233
+
234
+ ## 🧬 Design Choices
235
+
236
+ Two QUIC implementations can both be fully conformant and still behave completely
237
+ differently on the wire. RFC 9000 defines *what* must happen; it deliberately leaves
238
+ *how* to the implementer. This section documents where QUICO made a choice — the
239
+ places where another implementation would reasonably do something else.
240
+
241
+ ### The send model: range reconciliation, not retransmission
242
+
243
+ This is the foundational choice, and the one that most distinguishes QUICO from other
244
+ implementations.
245
+
246
+ **How it's normally done.** RFC 9002 describes tracking each packet you send along
247
+ with the frames it contained. When loss detection fires, you look up that packet,
248
+ recover its frames, and put them back in a retransmission queue. The sender is
249
+ *procedural*: an event happens (loss), and you perform a corrective action
250
+ (re-enqueue). Almost every QUIC stack works this way.
251
+
252
+ **What QUICO does instead.** There is no `sent_packets` table, no stored frame copies,
253
+ and no retransmission queue — those concepts don't exist in the codebase. Each stream
254
+ keeps only three things:
255
+
256
+ ```js
257
+ {
258
+ pending_data: Uint8Array, // the bytes the application wrote
259
+ acked_ranges: [from, to, …], // byte ranges the peer has confirmed
260
+ in_flight_ranges: { 47: [from, to], 48: [from, to] } // packet number → bytes it carries
261
+ }
262
+ ```
263
+
264
+ Every send opportunity asks one question, and it's a set operation:
265
+
266
+ ```js
267
+ missing = invert(acked_ranges ∪ in_flight_ranges, 0, total_size)
268
+ ```
269
+
270
+ Whatever's missing goes out. That's the whole algorithm. The sender doesn't remember
271
+ what it *did* — it recomputes what the peer *still needs*, from scratch, every time.
272
+ It's a declarative model: continuously reconcile "what the peer has" against "what the
273
+ peer should have", and emit the difference.
274
+
275
+ **Loss detection, therefore, is one line:**
276
+
277
+ ```js
278
+ delete st.in_flight_ranges[pn]; // → those bytes are "missing" again
279
+ ```
280
+
281
+ Deleting the mapping is the entire recovery action. Nothing is queued, scheduled, or
282
+ flagged. The bytes simply reappear in the next `missing` computation and get sent
283
+ along with everything else.
284
+
285
+ **The consequence: a packet is a disposable container, not an identity.** The data
286
+ that was in lost packet #47 doesn't come back *as* packet #47's contents. It comes
287
+ back as *bytes that need sending*, and the next burst may:
288
+
289
+ - **re-slice them differently** — the chunk boundaries depend on that burst's byte
290
+ budget, so a lost 1200-byte range might go out as 400 bytes appended to one packet
291
+ and 800 in the next;
292
+ - **coalesce them with new data** from the same stream, in a single STREAM frame;
293
+ - **pack them alongside other streams'** data in the same packet, via the round-robin
294
+ scheduler below.
295
+
296
+ Packet numbers and stream byte-offsets are fully decoupled: PNs are transient
297
+ bookkeeping for the loss timer, while the byte range is the only durable identity of
298
+ data. This is possible because QUIC frames carry explicit offsets — the receiver
299
+ reassembles by offset and genuinely does not care how the bytes were packetized.
300
+
301
+ **Why this design:**
302
+
303
+ - **State can't drift.** A retransmission queue is mutable state maintained
304
+ incrementally, and every incremental structure can desynchronize from reality —
305
+ entries duplicated, entries dropped, a range acked while a stale copy still sits in
306
+ the queue. Range arithmetic derives the truth fresh each pass, so a byte range can
307
+ neither be forgotten nor sent twice by bookkeeping error.
308
+ - **Recovery is free.** No priority juggling between "retransmit queue" and "new data
309
+ queue"; repairs and new data are the same thing, drawn from one computation and
310
+ paced by the same congestion controller.
311
+ - **Late information is harmless.** An ACK arriving after the loss timer already fired
312
+ isn't a contradiction to reconcile — it just adds a range to `acked_ranges`, and the
313
+ next pass naturally stops sending those bytes.
314
+
315
+ **What it costs.** Computing `invert(acked ∪ in_flight)` per stream per send
316
+ opportunity is O(number of ranges) work, where a queue pop would be O(1). With
317
+ coalesced flat range arrays this stays cheap in practice, but it is real CPU that a
318
+ queue-based sender doesn't spend — a deliberate trade of cycles for correctness.
319
+
320
+ **The receive side is symmetric.** Incoming data goes into `chunks` keyed by offset
321
+ plus a `ranges` array, and delivery walks the contiguous prefix. Same range algebra,
322
+ mirrored — which is exactly why the receiver must tolerate chunks whose boundaries
323
+ don't line up with anything it saw before: on the sending side, they never had to.
324
+
325
+ ### Congestion control: BBR-style, not loss-based
326
+
327
+ **The choice:** most QUIC stacks ship a loss-based controller — quic-go uses Reno with
328
+ HyStart++, others default to CUBIC. QUICO implements a **model-based controller in the
329
+ BBRv1 family** instead.
330
+
331
+ The difference is philosophical. Loss-based algorithms grow the window until packets
332
+ drop, which means they *require* queue overflow to find the limit — and they misread
333
+ random wireless loss as congestion. BBR builds an explicit model of the path and
334
+ steers by it:
335
+
336
+ ```
337
+ BDP = BtlBw × RTprop
338
+ ```
339
+
340
+ - **BtlBw** (bottleneck bandwidth) — a **windowed maximum** of per-round delivery rate
341
+ over the last 10 rounds. Sampling happens once per round-trip, not per ACK: under
342
+ ACK aggregation, per-ACK samples spike and a windowed-max latches onto the spike.
343
+ - **RTprop** (round-trip propagation) — a **windowed minimum** of RTT over 10 seconds,
344
+ so queue-induced inflation decays out of the model.
345
+
346
+ Three states drive the two output knobs:
347
+
348
+ | State | Pacing gain | Purpose |
349
+ |---|---|---|
350
+ | `startup` | 2.89 (≈ 2/ln 2) | Exponential ramp to discover BtlBw. Exits after 3 rounds without ≥25% growth — the pipe is full. |
351
+ | `drain` | 1/2.89 | One phase to drain the queue startup necessarily built. |
352
+ | `probe_bw` | `[1.25, 0.75, 1, 1, 1, 1, 1, 1]` | Steady state: probe for more bandwidth, then yield to drain what the probe queued. |
353
+
354
+ The **pacing rate** (`gain × BtlBw`) spreads packets smoothly rather than dumping
355
+ window-sized bursts into the bottleneck; the **congestion window** (`cwnd_gain × BDP`)
356
+ is the in-flight backstop. Both are clamped to floors and ceilings so a single bad
357
+ measurement can never wedge the connection.
358
+
359
+ Watch it live, one line per round-trip:
360
+
361
+ ```bash
362
+ QUICO_DEBUG_BBR=1 node yourserver.js
363
+ ```
364
+ ```
365
+ [bbr] round=5 startup BtlBw=8.96Mbps RTprop=16ms BDP=17920B → cwnd=51789B pace=25.89Mbps
366
+ [bbr] round=9 drain BtlBw=10.98Mbps RTprop=16ms BDP=21952B → cwnd=63441B pace=3.80Mbps
367
+ [bbr] round=15 probe_bw BtlBw=10.98Mbps RTprop=16ms BDP=21952B → cwnd=27440B pace=10.98Mbps
368
+ ```
369
+
370
+ That trace is a real run through a 10 Mbps bottleneck with a 25-packet queue: startup
371
+ finds the link in ~7 rounds, drain empties the queue, probe_bw settles within 5% of
372
+ line rate.
373
+
374
+ **Honest scope:** this is BBR-*style*, not a certified BBRv2. ProbeRTT — the periodic
375
+ cwnd collapse that re-measures RTprop on a permanently saturated path — is not
376
+ implemented, and there is no explicit loss response. The windowed filters bound the
377
+ damage of stale measurements in the meantime.
378
+
379
+ ### Stream scheduling: byte-level round-robin
380
+
381
+ **The choice:** when several streams have data ready and one packet is going out, who
382
+ gets the space? Implementations differ sharply — many drain streams in priority or
383
+ creation order, filling each one before moving on.
384
+
385
+ QUICO interleaves at the **byte level, inside a single packet**
386
+ ([`execute_quic_burst`](./src/quic_connection.js)). The scheduler cycles through all
387
+ active streams, taking a chunk from each until the packet is full, then cycles again —
388
+ so a 1350-byte packet routinely carries STREAM frames from three different streams:
389
+
390
+ ```
391
+ ┌─────────────────────────────── one QUIC packet ───────────────────────────────┐
392
+ │ ACK │ STREAM(id=0, 450B) │ STREAM(id=4, 450B) │ STREAM(id=8, 400B) │
393
+ └───────────────────────────────────────────────────────────────────────────────┘
394
+ ```
395
+
396
+ The loop repeats while any stream still yields data and space remains (`progress`
397
+ flag), so a stream that runs dry doesn't hold a slot — the remaining bandwidth
398
+ redistributes to the others within the same packet.
399
+
400
+ **Why it matters:** three parallel downloads progress at genuinely equal rates instead
401
+ of finishing sequentially, and no stream can be starved by a large one ahead of it.
402
+ The cost is fragmentation — more STREAM frame headers per packet (~24 bytes each) than
403
+ a drain-one-stream-at-a-time scheduler. QUICO accepts that trade for fairness.
404
+ Priority signalling (RFC 9218) would let applications override the equal split; it's
405
+ on the roadmap.
406
+
407
+ **The subtle part** falls straight out of the reconciliation model: the sender decides
408
+ what to send by asking which bytes aren't acked or in flight — but chunks selected
409
+ *earlier in the same burst* have no packet number yet, so they're in neither set, and
410
+ the next iteration would select them again. QUICO parks them under a temporary
411
+ `_burst` key in the in-flight map, which real packet-number keys replace the moment
412
+ the packet is encrypted and sent. The set arithmetic stays honest even mid-burst.
413
+
414
+ ### Within a stream: new data leads, gaps follow
415
+
416
+ Given the `missing` set, in what order do you drain it? QUICO walks it in two passes:
417
+ forward from the send cursor first (new data), then backfilling the gaps below it
418
+ (bytes whose packets were lost).
419
+
420
+ New data leads because it keeps the pipe full and the ACK clock running — the receiver
421
+ buffers out-of-order bytes anyway, and the gap gets repaired within the same burst
422
+ rather than a round-trip later. Both passes read from the same `missing` computation,
423
+ so there's no ordering conflict to arbitrate: it's one set, drained in two sweeps.
424
+
425
+ **Flow control interacts here in a non-obvious way.** Send-side limits cap the
426
+ *highest offset* we may send, so they bind only **new** bytes. A repair re-sends
427
+ offsets that were already inside the allowance when first sent — it consumes no budget
428
+ and must never be blocked. Getting this wrong deadlocks the connection: at the edge of
429
+ an exhausted window, the missing range itself becomes unsendable, and the peer can't
430
+ rescue you, because the data it would open the window for is exactly the data that
431
+ never arrived.
432
+
433
+ ### Loss detection: timers, and how we treat late ACKs
434
+
435
+ Because recovery itself is just the reconciliation pass, loss "detection" here means
436
+ only one thing: deciding when to stop counting a packet's bytes as in flight. Two
437
+ decisions worth calling out:
438
+
439
+ - **Late ACKs are honoured, not discarded.** When the timer expires a packet, its
440
+ stream ranges are parked in a *limbo* table rather than forgotten. If the ACK then
441
+ arrives late — routine on a queued path — the bytes are credited to the delivery
442
+ rate model, the pending retransmission is cancelled, and the loss counter is
443
+ corrected. QUIC never reuses packet numbers, so unlike TCP there's no
444
+ retransmission ambiguity: a late ACK is *unambiguous evidence* the data arrived, and
445
+ discarding it distorts BtlBw exactly when the model needs accuracy most.
446
+ - **RTT samples are taken from those packets too** — same reasoning, plus a subtle
447
+ failure mode it prevents: a too-short timer expires packets before their ACKs land,
448
+ and if those ACKs are then barred from producing RTT samples, the timer never learns
449
+ that it's too short. The loop is self-sealing. Accepting the sample breaks it.
450
+
451
+ The PTO itself is `srtt + max(4·rttvar, 1ms) + max_ack_delay`. That last term is not
452
+ optional: peers may legitimately sit on an ACK for their full advertised delay (25 ms
453
+ is common), and a timer without it expires packets moments before their ACKs arrive.
454
+
455
+ **ACK ranges are capped at 32** (RFC 9000 §13.2.4 permits acking a subset). Under
456
+ loss, packet-number gaps never close — a lost PN is gone forever, its data returns
457
+ under a new PN — so an uncapped ACK frame grows with the cumulative loss count until
458
+ it no longer fits in a packet. We measured exactly that against a line-rate sender:
459
+ ACK packets inflating 325 B → 1017 B second by second, until they crossed the MTU and
460
+ stopped being sendable at all.
461
+
462
+ ### Flow control: consumption-based windows
463
+
464
+ **The choice:** when do you tell the peer it may send more? The naive trigger is
465
+ "bytes arrived"; QUICO slides its receive windows based on **application
466
+ consumption** — a `MAX_STREAM_DATA` / `MAX_DATA` update goes out once the app has
467
+ actually *read* a threshold of data, not merely when packets landed.
468
+
469
+ The difference shows up under backpressure: a slow consumer causes the window to stop
470
+ advancing, which slows the sender, instead of letting unbounded data accumulate in the
471
+ receive buffer. Node stream semantics (`write()` return values, `highWaterMark`)
472
+ propagate through H3 into these windows, so ordinary `.pipe()` backpressure reaches
473
+ all the way to the peer's congestion controller.
474
+
475
+ ### Reading the source
476
+
477
+ Files map one-to-one onto protocol layers (see
478
+ [Project Structure](#-project-structure)), state lives in plain objects you can
479
+ `console.log`, and the dense parts carry comments explaining *why*, not just what.
480
+ Start at [`src/quic_connection.js`](./src/quic_connection.js) and follow a packet in
481
+ from `feedDatagram()` or out from `sendStream()`.
482
+
483
+ ---
484
+
485
+ ## 🔬 Interoperability
486
+
487
+ Everything above is a claim; this section is the receipt.
488
+
489
+ QUICO is tested with the [QUIC Interop Runner](https://github.com/quic-interop/quic-interop-runner),
490
+ the standard cross-implementation test harness. Every test runs both directions
491
+ (QUICO as server *and* as client) through a network simulator with configurable
492
+ delay, bandwidth limits and packet loss.
493
+
494
+ | Peer implementation | Language | `handshake` | `transfer` | `http3` |
495
+ |---|---|:---:|:---:|:---:|
496
+ | [quic-go](https://github.com/quic-go/quic-go) | Go | ✅ | ✅ | ✅ |
497
+ | [ngtcp2](https://github.com/ngtcp2/ngtcp2) | C | ✅ | ✅ | ✅ |
498
+ | [quiche](https://github.com/cloudflare/quiche) (Cloudflare) | Rust | ✅ | ✅ | ✅ |
499
+ | [picoquic](https://github.com/private-octopus/picoquic) | C | ✅ | ✅ | ✅ |
500
+ | [aioquic](https://github.com/aiortc/aioquic) | Python | ✅ | ✅ | ✅ |
501
+ | [neqo](https://github.com/mozilla/neqo) (Firefox) | Rust | ✅ | ✅ | ✅ |
502
+
503
+ - **handshake** — TLS 1.3 handshake over QUIC completes
504
+ - **transfer** — 10 MB across multiple concurrent streams, through a 10 Mbps
505
+ bottleneck with a shallow queue (i.e. with real packet loss)
506
+ - **http3** — three concurrent HTTP/3 requests, which must share **one** QUIC
507
+ connection
508
+
509
+ Additionally verified against production servers: **Google**, **Cloudflare**,
510
+ **Facebook**, **Microsoft**, **LiteSpeed** and **nginx** — including a 12.7 MB
511
+ download from `cloudflaremirrors.com` byte-for-byte.
512
+
513
+ ---
514
+
515
+ ## 📚 API Reference
516
+
517
+ QUICO mirrors the `node:https` API wherever possible. If a method or property is not
518
+ documented here, it likely behaves exactly as its `node:https` counterpart.
519
+
520
+ This chapter covers the HTTP/3 API first, then framework compatibility, WebTransport,
521
+ and finally the low-level QUIC primitives for non-HTTP protocols.
522
+
523
+ ### `quico.createServer([options][, requestListener])`
524
+
525
+ Creates an HTTP/3 server. Returns a server object with `listen()`, `close()` and
526
+ `on()`.
527
+
528
+ **Options**
529
+
530
+ | Option | Type | Default | Description |
531
+ |---|---|---|---|
532
+ | `key` | `Buffer \| string` | — | Private key in PEM format. Required unless `SNICallback` is used. |
533
+ | `cert` | `Buffer \| string` | — | Certificate chain in PEM format. |
534
+ | `ca` | `Buffer \| string \| Array` | `null` | Additional trusted CAs. |
535
+ | `SNICallback` | `Function` | `null` | `(servername, cb) => cb(null, secureContext)` for multi-domain setups. |
536
+ | `http2` | `boolean` | `true` | Also listen for HTTP/2 over TCP (fallback for non-QUIC clients). |
537
+ | `http1` | `boolean` | `true` | Also listen for HTTP/1.1 over TCP. |
538
+ | `maxConnections` | `number` | `10000` | Maximum concurrent QUIC connections. |
539
+ | `socket` | `dgram.Socket` | `null` | Use an externally-bound IPv4 UDP socket instead of creating one. Must already be bound. |
540
+ | `socket6` | `dgram.Socket` | `null` | Same, for IPv6. |
541
+
542
+ Setting `http2: false` and `http1: false` makes the server **HTTP/3 only** — no TCP
543
+ listener is opened.
544
+
545
+ **Methods**
546
+
547
+ - `server.listen(port[, host][, callback])` — start listening. Binds UDP (and TCP
548
+ unless both fallbacks are disabled).
549
+ - `server.close([callback])` — stop listening and close all connections.
550
+
551
+ **Events**
552
+
553
+ - `'request'` — `(req, res)` — same as the `requestListener` argument
554
+ - `'listening'`
555
+ - `'error'` — `(err)`
556
+
557
+ ```js
558
+ const server = quico.createServer({ key, cert, http2: false, http1: false });
559
+
560
+ server.on('request', (req, res) => {
561
+ res.writeHead(200, { 'content-type': 'application/json' });
562
+ res.end(JSON.stringify({ url: req.url, version: req.httpVersion }));
563
+ });
564
+
565
+ server.listen(4433, '0.0.0.0');
566
+ ```
567
+
568
+ ---
569
+
570
+ ### `quico.request(url|options[, options], callback)`
571
+
572
+ Issues an HTTP/3 request. Returns a [`ClientRequest`](#clientrequest). Accepts a URL
573
+ string, an options object, or both (options override the URL).
574
+
575
+ **Options**
576
+
577
+ | Option | Type | Default | Description |
578
+ |---|---|---|---|
579
+ | `hostname` / `host` | `string` | `'localhost'` | Target host. |
580
+ | `port` | `number` | `443` | Target port. |
581
+ | `path` | `string` | `'/'` | Request path including query string. |
582
+ | `method` | `string` | `'GET'` | HTTP method. |
583
+ | `headers` | `object` | `{}` | Request headers. |
584
+ | `rejectUnauthorized` | `boolean` | `true` | Verify the server certificate. Set `false` for self-signed certs. |
585
+ | `ca` | `Buffer \| string \| Array` | `null` | Trust these CAs in addition to the defaults. |
586
+ | `agent` | `Agent \| false` | `globalAgent` | Connection pool to use. `false` disables pooling. |
587
+ | `http2` | `boolean` | `true` | Allow HTTP/2 fallback. |
588
+ | `http1` | `boolean` | `true` | Allow HTTP/1.1 fallback. |
589
+ | `h3Timeout` | `number` | `3000` | Milliseconds to wait for QUIC before falling back. |
590
+
591
+ ```js
592
+ const req = quico.request({
593
+ hostname: 'localhost',
594
+ port: 4433,
595
+ path: '/api/items',
596
+ method: 'POST',
597
+ headers: { 'content-type': 'application/json' },
598
+ rejectUnauthorized: false // self-signed cert in development
599
+ }, (res) => {
600
+ let body = '';
601
+ res.on('data', (c) => body += c);
602
+ res.on('end', () => console.log(res.statusCode, body));
603
+ });
604
+
605
+ req.on('error', console.error);
606
+ req.write(JSON.stringify({ name: 'test' }));
607
+ req.end();
608
+ ```
609
+
610
+ ### `quico.get(url|options[, options], callback)`
611
+
612
+ Same as `request()` with `method: 'GET'`, and `end()` is called automatically.
613
+
614
+ ---
615
+
616
+ ### `IncomingMessage`
617
+
618
+ A Node.js `Readable` stream. On the server it is the request; on the client, the
619
+ response.
620
+
621
+ | Property | Description |
622
+ |---|---|
623
+ | `httpVersion` | `'3.0'`, `'2.0'` or `'1.1'` depending on the transport actually used |
624
+ | `headers` | Lower-cased header object |
625
+ | `method` | Request method (server side) |
626
+ | `url` | Request path (server side) |
627
+ | `statusCode` | Response status (client side) |
628
+ | `socket` / `connection` | Compatibility shim for frameworks |
629
+
630
+ Standard `Readable` behaviour applies: `'data'`, `'end'`, `'error'`, `pipe()`,
631
+ `for await (const chunk of req)`, and `setTimeout(ms, cb)`.
146
632
 
633
+ ---
634
+
635
+ ### `ServerResponse`
636
+
637
+ A Node.js `Writable` stream.
638
+
639
+ - `res.writeHead(statusCode[, statusMessage][, headers])`
640
+ - `res.setHeader(name, value)` / `getHeader` / `getHeaders` / `getHeaderNames` /
641
+ `hasHeader` / `removeHeader`
642
+ - `res.flushHeaders()`
643
+ - `res.write(chunk)` / `res.end([chunk])`
644
+ - `res.statusCode`, `res.headersSent`
147
645
 
148
- ## 🔗 Node.js API Compatibility
646
+ For WebTransport sessions, `ServerResponse` additionally exposes
647
+ `sendDatagram()`, `createBidirectionalStream()` and `createUnidirectionalStream()` —
648
+ see [WebTransport](#-webtransport).
149
649
 
150
- One of the core design goals of QUICO is to provide a **familiar API**. If you already know `node:https`, working with QUICO should feel natural — `createServer`, `request`, `req`, `res` all work the same way.
650
+ ---
651
+
652
+ ### `ClientRequest`
151
653
 
152
- This means existing frameworks work out of the box:
654
+ A Node.js `Writable` stream.
655
+
656
+ - `req.write(chunk)` / `req.end([chunk])`
657
+ - `req.abort()` / `req.destroy()`
658
+ - `req.protocol` — which transport was actually used: `'h3'`, `'h2'` or `'https'`
659
+ - Events: `'response'`, `'error'`
660
+
661
+ ---
662
+
663
+ ### `Agent`
664
+
665
+ Connection pool and protocol cache, analogous to `https.Agent`.
666
+
667
+ ```js
668
+ import { Agent } from 'quico';
669
+
670
+ const agent = new Agent({
671
+ http2: true, // allow H2 in the fallback chain
672
+ http1: true, // allow H1.1 in the fallback chain
673
+ h3Timeout: 3000, // ms before falling back from H3
674
+ maxSockets: 256,
675
+ timeout: 30000
676
+ });
677
+
678
+ quico.request({ hostname: 'example.com', agent }, handler).end();
679
+ ```
680
+
681
+ The agent caches which protocol works per host (including `Alt-Svc` advertisements
682
+ with their `ma=` lifetime), pools live QUIC connections for multiplexing, and
683
+ coalesces concurrent requests to the same origin onto a single connection.
684
+
685
+ `quico.globalAgent` is the default instance used when no `agent` option is given.
686
+ Call `agent.destroy()` to close all pooled connections.
687
+
688
+ ---
689
+
690
+ ### `quico.createSecureContext(options)`
691
+
692
+ Creates a TLS context for use with `SNICallback`. Accepts `key`, `cert` and `ca`.
693
+ Re-exported from LemonTLS.
694
+
695
+ ---
696
+
697
+ ### Framework compatibility
698
+
699
+ A core design goal is a **familiar API**. If you know `node:https`, QUICO should feel
700
+ natural — which means existing frameworks work unmodified:
153
701
 
154
702
  ```js
155
703
  import express from 'express';
@@ -162,17 +710,20 @@ quico.createServer({ key: KEY, cert: CERT }, app).listen(4433);
162
710
  // → { "protocol": "3.0" }
163
711
  ```
164
712
 
165
- For multi-domain setups, QUICO supports `SNICallback` just like `node:https`:
713
+ Express, Fastify and Koa are all supported this way.
714
+
715
+ Multi-domain setups use `SNICallback`, exactly as in `node:https`:
166
716
 
167
717
  ```js
168
718
  import quico from 'quico';
169
719
  import tls from 'lemon-tls';
720
+ import fs from 'node:fs';
170
721
 
171
722
  const server = quico.createServer({
172
- SNICallback: function (servername, cb) {
723
+ SNICallback: (servername, cb) => {
173
724
  cb(null, tls.createSecureContext({
174
- key: fs.readFileSync('certs/' + servername + '.key'),
175
- cert: fs.readFileSync('certs/' + servername + '.crt')
725
+ key: fs.readFileSync(`certs/${servername}.key`),
726
+ cert: fs.readFileSync(`certs/${servername}.crt`)
176
727
  }));
177
728
  }
178
729
  }, (req, res) => {
@@ -180,49 +731,39 @@ const server = quico.createServer({
180
731
  res.end('Hello World from HTTP/3 on Node.js!');
181
732
  });
182
733
 
183
- server.listen(4433, () => {
184
- console.log('🚀 QUIC server running on https://localhost:4433');
185
- });
734
+ server.listen(4433);
186
735
  ```
187
736
 
737
+ ---
188
738
 
189
- ## 🚀 WebTransport
739
+ ### WebTransport
190
740
 
191
- ### Server
741
+ #### Server
192
742
 
193
743
  ```js
194
744
  import quico from 'quico';
195
745
 
196
746
  quico.createServer({ key: KEY, cert: CERT }, (req, res) => {
197
747
  if (req.headers[':protocol'] === 'webtransport') {
198
- // Accept the WebTransport session
199
- res.writeHead(200);
200
-
201
- // Bidirectional streams from the client
202
- req.on('stream', (stream) => {
203
- stream.on('data', (chunk) => {
204
- stream.write(chunk); // echo back
205
- });
748
+ res.writeHead(200); // accept the session
749
+
750
+ req.on('stream', (stream) => { // client-initiated streams
751
+ stream.on('data', (chunk) => stream.write(chunk)); // echo
206
752
  stream.on('end', () => stream.end());
207
753
  });
208
754
 
209
- // Unreliable datagrams
210
- req.on('datagram', (data) => {
211
- res.sendDatagram(data); // echo back
212
- });
755
+ req.on('datagram', (data) => res.sendDatagram(data));
213
756
 
214
- // Server can also create streams
215
- const push = res.createBidirectionalStream();
757
+ const push = res.createBidirectionalStream(); // server-initiated
216
758
  push.write('server says hello');
217
759
  push.end();
218
760
  } else {
219
- // Regular HTTP/3 request
220
761
  res.end('Hello HTTP/3!');
221
762
  }
222
763
  }).listen(4433);
223
764
  ```
224
765
 
225
- ### Client (Node.js)
766
+ #### Client (Node.js)
226
767
 
227
768
  ```js
228
769
  import { WebTransport } from 'quico';
@@ -230,111 +771,283 @@ import { WebTransport } from 'quico';
230
771
  const wt = new WebTransport('https://localhost:4433/echo');
231
772
  await wt.ready;
232
773
 
233
- // Bidirectional stream
234
774
  const stream = await wt.createBidirectionalStream();
235
775
  stream.write('hello');
236
776
  stream.on('data', (chunk) => console.log('echo:', chunk.toString()));
237
777
  stream.end();
238
778
 
239
- // Unidirectional stream
240
779
  const uni = await wt.createUnidirectionalStream();
241
780
  uni.write('fire and forget');
242
781
  uni.end();
243
782
 
244
- // Datagrams (unreliable)
245
783
  wt.sendDatagram(Buffer.from('ping'));
246
784
  wt.on('datagram', (data) => console.log('pong:', data.toString()));
247
785
 
248
786
  wt.close();
249
787
  ```
250
788
 
251
- ### Client (Browser)
789
+ #### Client (Browser)
790
+
791
+ A QUICO server works with the **native browser WebTransport API** — no client library
792
+ needed:
252
793
 
253
794
  ```js
254
795
  const wt = new WebTransport('https://yourserver.com:4433/live');
255
796
  await wt.ready;
256
797
 
257
- // Send via WHATWG Streams API
258
798
  const writer = wt.datagrams.writable.getWriter();
259
799
  await writer.write(new TextEncoder().encode('Hello QUIC'));
260
800
 
261
- // Receive
262
801
  const reader = wt.datagrams.readable.getReader();
263
802
  const { value } = await reader.read();
264
803
  console.log('Received:', new TextDecoder().decode(value));
265
804
  ```
266
805
 
806
+ ---
807
+
808
+ ### Low-level QUIC API
809
+
810
+ For non-HTTP protocols over QUIC, or full control of the connection.
811
+
812
+ #### `createQuicServer(options)`
813
+
814
+ | Option | Type | Default | Description |
815
+ |---|---|---|---|
816
+ | `key`, `cert`, `ca` | | — | TLS credentials |
817
+ | `SNICallback` | `Function` | `null` | Per-hostname contexts |
818
+ | `alpn` | `string[]` | `['h3']` | ALPN protocols to accept |
819
+ | `maxConnections` | `number` | `10000` | Connection cap |
820
+ | `socket` / `socket6` | `dgram.Socket` | `null` | External, already-bound UDP sockets |
821
+
822
+ Returns `{ listen, close, on, handlePacket, hasConnection }`. The `'connection'`
823
+ event fires with `(quicConnection, peer)`.
824
+
825
+ ```js
826
+ import { createQuicServer } from 'quico';
827
+
828
+ const qs = createQuicServer({ key, cert, alpn: ['my-protocol'] });
829
+
830
+ qs.on('connection', (quic, peer) => {
831
+ console.log('connection from', peer.address, peer.port);
832
+
833
+ quic.on('stream', (streamId, data, fin) => {
834
+ if (fin) quic.sendStream(streamId, Buffer.from('bye'), true);
835
+ });
836
+
837
+ quic.on('datagram', (data) => quic.sendDatagram(data));
838
+ });
839
+
840
+ qs.listen(4433);
841
+ ```
842
+
843
+ `handlePacket(msg, rinfo)` and `hasConnection(rinfo)` support **external socket
844
+ mode**, where QUICO shares a UDP port with another protocol (STUN/TURN, for example)
845
+ and you route datagrams yourself.
846
+
847
+ #### `QUICConnection`
848
+
849
+ | Member | Description |
850
+ |---|---|
851
+ | `quic.state` | `'idle'`, `'handshaking'`, `'connected'`, `'closing'`, `'draining'`, `'closed'` |
852
+ | `quic.sendStream(id, data, fin)` | Write to a stream |
853
+ | `quic.sendDatagram(data)` | Send an unreliable datagram |
854
+ | `quic.maxDatagramSize()` | Largest datagram payload that fits the current path |
855
+ | `quic.close(errorCode, reason)` | Send CONNECTION_CLOSE |
856
+ | `quic.on(event, fn)` / `off` | Event subscription |
857
+
858
+ **Events:** `'connect'`, `'stream'` (`id, data, fin`), `'datagram'`, `'close'`,
859
+ `'error'`, `'keylog'`.
860
+
861
+ **Connection options** (when constructing directly): `idleTimeout` (default `30000`),
862
+ `handshakeTimeout` (default `10000`), `keepAlive` (`true` → half the idle timeout, or
863
+ a number of ms), `alpn`, `hostname`, `rejectUnauthorized`, `ca`.
864
+
865
+ ---
866
+
867
+
868
+
869
+ ---
870
+
871
+ ## ⚙️ Configuration & Tuning
872
+
873
+ ### Transport parameters
874
+
875
+ These are the values QUICO advertises to peers. They are currently compiled-in
876
+ defaults; making them configurable is on the roadmap.
877
+
878
+ | Parameter | Default | Meaning |
879
+ |---|---|---|
880
+ | `initial_max_data` | 1 MB | Connection-level receive window |
881
+ | `initial_max_stream_data_bidi_local/remote` | 256 KB | Per-stream receive window |
882
+ | `initial_max_stream_data_uni` | 128 KB | Per-unidirectional-stream window |
883
+ | `initial_max_streams_bidi` | 100 | Concurrent bidirectional streams |
884
+ | `initial_max_streams_uni` | 3 | Concurrent unidirectional streams |
885
+ | `max_idle_timeout` | 30 s | Idle before the connection is dropped |
886
+ | `max_ack_delay` | 25 ms | Maximum ACK delay we advertise |
887
+ | `max_datagram_frame_size` | 65527 | Datagram support (RFC 9221) |
888
+ | `active_connection_id_limit` | 4 | Connection IDs we will track |
889
+
890
+ Receive windows slide automatically as the application consumes data, so large
891
+ downloads are not limited by the initial values.
892
+
893
+ ### Per-connection timers
894
+
895
+ ```js
896
+ // via the low-level API
897
+ new QUICConnection({
898
+ idleTimeout: 60000, // default 30000
899
+ handshakeTimeout: 10000, // default 10000
900
+ keepAlive: true // PING at idleTimeout/2; or a number of ms
901
+ });
902
+ ```
903
+
904
+ ---
905
+
906
+ ## 🐛 Debugging
907
+
908
+ ### Environment variables
909
+
910
+ | Variable | Effect |
911
+ |---|---|
912
+ | `QUICO_DEBUG=1` | Full per-packet trace: every datagram, packet, frame, stream write, and TLS transition. **Very verbose** — it can become the bottleneck it observes, so don't use it for throughput measurements. |
913
+ | `QUICO_DEBUG_BBR=1` | One line per round-trip with congestion-control state: `BtlBw`, `RTprop`, `BDP`, `cwnd`, pacing rate. Cheap enough to leave on. |
914
+
915
+ ```bash
916
+ QUICO_DEBUG_BBR=1 node server.js
917
+ # [bbr] round=12 probe_bw BtlBw=9.84Mbps RTprop=16ms BDP=19671B → cwnd=24589B pace=9.84Mbps
918
+ ```
919
+
920
+ ### Decrypting traffic in Wireshark
921
+
922
+ QUICO emits TLS secrets in NSS keylog format, so you can read your own encrypted
923
+ QUIC traffic:
924
+
925
+ ```js
926
+ import fs from 'node:fs';
927
+
928
+ const out = fs.createWriteStream('/tmp/keys.log', { flags: 'a' });
929
+ quic.on('keylog', (line) => out.write(line)); // low-level QUICConnection
930
+ ```
931
+
932
+ Then in Wireshark, set *Preferences → Protocols → TLS → (Pre)-Master-Secret log
933
+ filename* to that file — or from the command line:
934
+
935
+ ```bash
936
+ tshark -r capture.pcap -o tls.keylog_file:/tmp/keys.log -Y quic
937
+ ```
938
+
939
+ ---
267
940
 
268
941
  ## 🧪 Testing
269
942
 
270
- QUIC requires TLS 1.3. Self-signed RSA certificates usually fail with QUIC clients and browsers. For local development, [mkcert](https://github.com/FiloSottile/mkcert) is the easiest option — it generates locally-trusted ECDSA certificates automatically.
943
+ QUIC requires TLS 1.3. Self-signed RSA certificates are often rejected by QUIC clients
944
+ and browsers; [mkcert](https://github.com/FiloSottile/mkcert) is the easiest way to get
945
+ locally-trusted ECDSA certificates.
271
946
 
272
- This project supports ALPN `"h3"` only (not draft variants like `h3-29`). You must explicitly force h3 when testing with Chrome.
947
+ QUICO supports ALPN `"h3"` only — not draft variants such as `h3-29`. Chrome must be
948
+ told explicitly to use QUIC against a local server:
273
949
 
274
- **Chrome:**
950
+ **Chrome**
275
951
  ```bash
276
- chrome --enable-quic --quic-version=h3 --ignore-certificate-errors --origin-to-force-quic-on=localhost:4433
952
+ chrome --enable-quic --quic-version=h3 --ignore-certificate-errors \
953
+ --origin-to-force-quic-on=localhost:4433
277
954
  ```
278
955
 
279
- **curl:**
956
+ **curl** (needs an HTTP/3-enabled build)
280
957
  ```bash
281
958
  curl --http3 https://localhost:4433 --insecure
282
959
  ```
283
960
 
284
- **Integration tests:**
961
+ **Test suites**
285
962
  ```bash
286
- node examples/test_integration.js # HTTP/3 client + server — 19 tests
287
- node examples/test_wt_server.js # WebTransport server echo (QUICO ↔ QUICO)
288
- node examples/test_webtransport.js URL # WebTransport client against any server
963
+ node examples/test_integration.js # HTTP/3 client + server — 19 tests
964
+ node examples/test_wt_server.js # WebTransport echo (QUICO ↔ QUICO)
965
+ node examples/test_webtransport.js URL # WebTransport client against any server
289
966
  ```
290
967
 
968
+ **Interop runner** — see [`interop/`](./interop) for the Docker endpoint used with the
969
+ [QUIC Interop Runner](https://github.com/quic-interop/quic-interop-runner).
970
+
971
+ ---
291
972
 
292
973
  ## 📁 Project Structure
293
974
 
294
975
  ```
295
976
  quico/
296
977
  ├── index.js — Public API exports
978
+ ├── interop/ — QUIC Interop Runner endpoint (Dockerfile + shim)
979
+ ├── examples/ — Runnable examples and test suites
297
980
  └── src/
298
981
  ├── server.js — Unified server (drop-in for node:https)
299
982
  ├── client.js — Unified client with H3 → H2 → H1 fallback
300
- ├── agent.js — Connection pooling and Alt-Svc protocol cache
983
+ ├── agent.js — Connection pooling, coalescing, Alt-Svc cache
301
984
  ├── streams.js — IncomingMessage, ServerResponse, ClientRequest
302
985
  ├── h3.js — HTTP/3 framing, QPACK, WebTransport detection
303
986
  ├── h3_server.js — HTTP/3 + WebTransport server
304
987
  ├── h3_client.js — HTTP/3 client (DNS, UDP, QUIC, H3 pipeline)
305
- ├── quic_connection.js — QUIC state machine, burst scheduler, flow control
988
+ ├── quic_server.js — UDP listener, connection routing, retry/VN
989
+ ├── quic_socket.js — Client-side UDP socket + connection bootstrap
990
+ ├── quic_connection.js — QUIC state machine, loss detection, CC, flow control
306
991
  ├── transport.js — Packet parsing, frame encoding/decoding
307
- ├── crypto.js — AEAD encryption, header protection, key derivation
992
+ ├── crypto.js — AEAD, header protection, key derivation
308
993
  ├── tls_bridge.js — LemonTLS integration, transport parameters
994
+ ├── utils.js — VarInt, ACK ranges, binary helpers
309
995
  └── webtransport.js — WebTransport client + stream classes
310
996
  ```
311
997
 
998
+ ---
999
+
1000
+ ## ⚠️ Known Limitations
1001
+
1002
+ Being explicit about what is **not** implemented yet:
1003
+
1004
+ | Area | Status |
1005
+ |---|---|
1006
+ | **0-RTT / session resumption** | Not implemented — every connection is a full handshake. |
1007
+ | **ChaCha20-Poly1305** | Not implemented. AES-128-GCM and AES-256-GCM only. On hardware without AES acceleration this costs performance. |
1008
+ | **Connection migration** | Not implemented; QUICO advertises `disable_active_migration`. Connections do not survive a client IP change. |
1009
+ | **ECN** | Not implemented — Node's `dgram` does not expose the required socket options. |
1010
+ | **Retry / address validation** | Not implemented (client does not consume Retry tokens; server does not issue them). |
1011
+ | **Anti-amplification limit** | Not enforced (RFC 9000 §8.1). |
1012
+ | **Stream priority** (RFC 9218) | Not implemented. |
1013
+ | **Delayed ACK** | Not implemented — an ACK is sent per ack-eliciting packet. Correct, but more packets than necessary. |
1014
+ | **`MAX_STREAMS` issuance** | Streams limits are honoured but not proactively raised, capping very-high-concurrency workloads. |
1015
+ | **TypeScript types** | Not shipped yet. |
1016
+
1017
+ Performance has not been formally benchmarked. QUICO is pure JavaScript; expect it to
1018
+ be slower than native implementations, especially for bulk transfer.
1019
+
1020
+ ---
312
1021
 
313
1022
  ## 🛣 Roadmap
314
1023
 
315
1024
  ### ✅ Done
316
1025
  - QUIC v1 transport (RFC 9000) — Initial, Handshake, 1-RTT
317
1026
  - TLS 1.3 via LemonTLS — X25519, P-256, P-384, AES-128/256-GCM
1027
+ - Version Negotiation, HelloRetryRequest
1028
+ - Key Update — bidirectional key phase rotation (RFC 9001 §6)
1029
+ - Loss detection, PTO with exponential backoff (RFC 9002)
1030
+ - BBR-style congestion control with pacing
1031
+ - Bidirectional flow control with sliding windows
318
1032
  - HTTP/3 server and client (RFC 9114)
319
- - QPACK header compression with static and dynamic tables (RFC 9204)
320
- - WebTransport client and server — bidi/uni streams + datagrams (RFC 9297)
321
- - Node.js `https`-style API compatibility — `createServer`, `request`, `get`
322
- - Express / Fastify / Koa middleware support
323
- - Connection reuse and multiplexing
324
- - HelloRetryRequest — client and server
325
- - Key updates (key phase rotation)
326
- - Interop with Google, Cloudflare, Facebook, Microsoft, nginx
327
- - Handshake timeout
1033
+ - QPACK with static and dynamic tables (RFC 9204)
1034
+ - WebTransport client and server — streams + datagrams (RFC 9297)
1035
+ - Unreliable datagrams (RFC 9221)
1036
+ - `node:https`-style API — `createServer`, `request`, `get`, `Agent`
1037
+ - Express / Fastify / Koa support
1038
+ - Connection pooling, coalescing and multiplexing
1039
+ - TLS keylog export
1040
+ - **Interop: 6/6 implementations green** (handshake, transfer, http3, both directions)
328
1041
 
329
1042
  ### 🔄 In Progress
330
- - Loss detection and PTO timers (RFC 9002)
331
- - Congestion control (BBR / CUBIC)
1043
+ - Delayed ACK and ACK piggybacking
332
1044
  - 0-RTT and session resumption
333
- - Debug log cleanup (`QUICO_DEBUG` environment flag)
1045
+ - Configurable transport parameters
334
1046
 
335
1047
  ### ⏳ Planned
336
1048
  - Connection migration (PATH_CHALLENGE / PATH_RESPONSE)
337
- - ChaCha20-Poly1305 cipher suite (0x1303)
1049
+ - ChaCha20-Poly1305 (0x1303)
1050
+ - Retry / address validation + anti-amplification limit
338
1051
  - GOAWAY and graceful shutdown
339
1052
  - Stream priority (RFC 9218)
340
1053
  - TypeScript type definitions
@@ -343,23 +1056,39 @@ quico/
343
1056
 
344
1057
  _Community contributions are welcome! Please ⭐ star the repo to follow progress._
345
1058
 
1059
+ ---
346
1060
 
347
- ## 🙏 Sponsors
1061
+ ## 🤝 Contributing
1062
+
1063
+ Contributions are very welcome — bug reports, reproductions, and PRs alike.
1064
+
1065
+ Useful things to know before you start:
1066
+
1067
+ - Run `node examples/test_integration.js` before and after your change (19 tests).
1068
+ - `QUICO_DEBUG=1` gives a full packet trace; `QUICO_DEBUG_BBR=1` gives a congestion
1069
+ control timeline. Include the relevant excerpt in bug reports.
1070
+ - If you can capture a `.pcap` plus a keylog file, that makes protocol bugs enormously
1071
+ easier to diagnose — see [Debugging](#-debugging).
1072
+ - For interop-level changes, the [`interop/`](./interop) directory contains the Docker
1073
+ endpoint used with the QUIC Interop Runner.
348
1074
 
349
- QUICO is an evenings-and-weekends project.
350
- Support development via **GitHub Sponsors** or simply share the project.
1075
+ ---
1076
+
1077
+ ## 🙏 Sponsors
351
1078
 
1079
+ QUICO is an evenings-and-weekends project.
1080
+ Support development via **GitHub Sponsors** — or simply share the project.
352
1081
 
1082
+ ---
353
1083
 
354
1084
  ## 💬 Contact
355
1085
 
356
- For feedback, ideas, or contributions,
357
- contact directly at:
1086
+ For feedback, ideas, or contributions:
358
1087
  📧 **support@quicojs.dev**
359
1088
 
360
1089
  For security-related issues, please see [SECURITY.md](./SECURITY.md).
361
1090
 
362
-
1091
+ ---
363
1092
 
364
1093
  ## 📜 License
365
1094
 
@@ -379,4 +1108,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
379
1108
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
380
1109
  See the License for the specific language governing permissions and
381
1110
  limitations under the License.
382
- ```
1111
+ ```