quico 0.4.0 → 0.7.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/src/h2.js ADDED
@@ -0,0 +1,1325 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import { HpackEncoder, HpackDecoder } from './hpack.js';
16
+ import * as F from './h2_frames.js';
17
+ import {
18
+ Emitter, parseContentLength, validateFieldSection, parsePriority, formatPriority
19
+ } from './http_common.js';
20
+ import { mergePriority } from './utils.js';
21
+
22
+
23
+ // ============================================================
24
+ // H2Connection — HTTP/2 over one byte stream (RFC 9113).
25
+ //
26
+ // The same position in the stack as h3.js: it is handed ordered bytes and
27
+ // turns them into request/response events, knowing nothing about sockets.
28
+ // What it must do that h3.js does not is everything QUIC provides for free —
29
+ // streams, their state machine, and flow control in both directions. In
30
+ // HTTP/3 those are the transport's; here they are the protocol's own, which
31
+ // is most of the reason this file is four times the size of h3.js's framing.
32
+ //
33
+ // Events:
34
+ // 'headers' (streamId, headers, endStream)
35
+ // 'data' (streamId, chunk)
36
+ // 'end' (streamId)
37
+ // 'rst' (streamId, errorCode)
38
+ // 'settings' (settings)
39
+ // 'goaway' (lastStreamId, errorCode, debug)
40
+ // 'ping' (payload)
41
+ // 'send' (buffer) — bytes to put on the wire
42
+ // 'error' (err) — connection error; the connection is over
43
+ // 'drain' (streamId) — a stream's send window reopened
44
+ // 'stream_close' (streamId) — a stream that was open is closed (both
45
+ // ends, or reset); `activeStreams` counts the rest
46
+ // 'window_blocked' (streamId) — a stream's DATA waits for the peer to
47
+ // open a window (WINDOW_UPDATE)
48
+ // ============================================================
49
+
50
+ var STATE = {
51
+ IDLE: 'idle',
52
+ RESERVED_LOCAL: 'reserved_local',
53
+ RESERVED_REMOTE: 'reserved_remote',
54
+ OPEN: 'open',
55
+ HALF_CLOSED_LOCAL: 'half_closed_local',
56
+ HALF_CLOSED_REMOTE: 'half_closed_remote',
57
+ CLOSED: 'closed'
58
+ };
59
+
60
+ // Pseudo-header sets and the connection-specific field list live in
61
+ // http_common.js: RFC 9114 §4.2 names exactly the same fields for HTTP/3, so
62
+ // h3.js will want them too rather than a second copy that can drift.
63
+
64
+
65
+ function H2Connection(options) {
66
+ if (!(this instanceof H2Connection)) return new H2Connection(options);
67
+ options = options || {};
68
+
69
+ var isServer = options.isServer !== false;
70
+ // The server's counters (server_stats.js), when its server keeps them.
71
+ var stats = options.stats || null;
72
+
73
+ var ev = Emitter();
74
+ var on = ev.on, emit = ev.emit, off = ev.off;
75
+
76
+ // ---- Settings ----
77
+ var localSettings = {
78
+ HEADER_TABLE_SIZE: options.headerTableSize != null ? options.headerTableSize : 4096,
79
+ ENABLE_PUSH: 0, // we never accept push; see below
80
+ // Node's own HTTP/2 server advertises 2^32-1 here, i.e. no limit, and
81
+ // browsers are tuned against that. Advertising 100 is defensible but it is
82
+ // a much lower ceiling than anything a browser expects to meet, and a
83
+ // client that is counting streams against it may decline to open one it
84
+ // would otherwise have opened. Matching Node removes that variable; a
85
+ // caller that wants a real limit can still set one.
86
+ // 100 is what browsers, nghttp2 and most public servers settle on, and it
87
+ // is what h2spec §5.1.2 exercises: a server advertising 2^32-1 is saying
88
+ // there is no limit, and the conformance suite duly tries to open that
89
+ // many streams. Node's own server does advertise 2^32-1, but Node is not
90
+ // trying to be a public-facing server by default.
91
+ //
92
+ // Worth being deliberate about rather than inherited: this is the only
93
+ // backstop against a peer opening unbounded streams, and each stream costs
94
+ // memory that the flow-control window does not bound. A caller that wants
95
+ // a different ceiling can now set one — that option used to be dropped
96
+ // before it reached this layer.
97
+ MAX_CONCURRENT_STREAMS: options.maxConcurrentStreams != null ? options.maxConcurrentStreams : 100,
98
+ INITIAL_WINDOW_SIZE: options.initialWindowSize != null ? options.initialWindowSize : 65535,
99
+ MAX_FRAME_SIZE: options.maxFrameSize != null ? options.maxFrameSize : 16384,
100
+ MAX_HEADER_LIST_SIZE: options.maxHeaderListSize != null ? options.maxHeaderListSize : 128 * 1024,
101
+ // RFC 8441. A server advertises it so clients may run WebSocket over this
102
+ // connection; a client never does (it has nothing to offer a server), so
103
+ // it is only included below when isServer.
104
+ ENABLE_CONNECT_PROTOCOL: (isServer && options.enableConnectProtocol !== false) ? 1 : 0
105
+ };
106
+ // RFC 9218 §2.1: the server reads RFC 9218's signals, not RFC 7540's tree.
107
+ // A server's only: a client's SETTINGS are left as they were (a client
108
+ // profile may be matching a browser's exactly).
109
+ if (isServer) localSettings.NO_RFC7540_PRIORITIES = 1;
110
+
111
+ var remoteSettings = {
112
+ HEADER_TABLE_SIZE: 4096,
113
+ ENABLE_PUSH: 1,
114
+ MAX_CONCURRENT_STREAMS: Infinity,
115
+ INITIAL_WINDOW_SIZE: F.DEFAULT_WINDOW_SIZE,
116
+ MAX_FRAME_SIZE: F.DEFAULT_MAX_FRAME_SIZE,
117
+ MAX_HEADER_LIST_SIZE: Infinity,
118
+ ENABLE_CONNECT_PROTOCOL: 0
119
+ };
120
+
121
+ var encoder = new HpackEncoder({ maxTableSize: remoteSettings.HEADER_TABLE_SIZE });
122
+ var decoder = new HpackDecoder({
123
+ maxTableSize: localSettings.HEADER_TABLE_SIZE,
124
+ maxHeaderListSize: localSettings.MAX_HEADER_LIST_SIZE
125
+ });
126
+
127
+ // ---- Connection state ----
128
+ var buf = Buffer.alloc(0);
129
+ var prefaceReceived = !isServer; // a server waits for it, a client sends it
130
+ var streams = {};
131
+ var lastRemoteStreamId = 0;
132
+ var nextLocalStreamId = isServer ? 2 : 1;
133
+ var goawaySent = false;
134
+ var goawayLastStreamId = 0; // the last-stream-id our GOAWAY carried
135
+ // Streams counted against MAX_CONCURRENT_STREAMS, kept as a count: it was
136
+ // recomputed by walking every stream record on every new HEADERS.
137
+ var activeCount = 0;
138
+ // Whether the transport below can take more right now (the socket's buffer
139
+ // is not over its mark). DATA waits while it cannot; see flushStream.
140
+ var isWriteBlocked = typeof options.isWriteBlocked === 'function' ? options.isWriteBlocked : function () { return false; };
141
+ var goawayReceived = false;
142
+ var closed = false;
143
+ var settingsAcked = false;
144
+
145
+ // Connection-level flow control. Two independent windows per direction: one
146
+ // for the connection, one per stream, and a DATA frame consumes from BOTH.
147
+ //
148
+ // Both start at 65535 and NOT at INITIAL_WINDOW_SIZE. RFC 9113 §6.9.2 is
149
+ // explicit that SETTINGS_INITIAL_WINDOW_SIZE governs stream windows only and
150
+ // does not change the connection window, which moves solely by WINDOW_UPDATE
151
+ // on stream 0. Seeding it from the setting instead makes a server that
152
+ // advertises a small per-stream window unable to receive anything at all —
153
+ // the connection window runs out first and every request dies with a
154
+ // connection-level FLOW_CONTROL_ERROR that looks like the peer's fault.
155
+ var localWindow = F.DEFAULT_WINDOW_SIZE; // what we may receive
156
+ var remoteWindow = F.DEFAULT_WINDOW_SIZE; // what we may send
157
+
158
+ // What we widen the connection receive window to in our first flight; see
159
+ // sendPreface. Configurable because a proxy fanning out thousands of
160
+ // connections may not want 16MB of credit outstanding on each.
161
+ var CONNECTION_WINDOW = options.connectionWindowSize != null
162
+ ? options.connectionWindowSize
163
+ : 16 * 1024 * 1024;
164
+
165
+ // A header block may be split across HEADERS + CONTINUATION, and §6.10
166
+ // forbids ANY other frame in between — including one for a different stream.
167
+ // Tracking it as connection state rather than per-stream is the point: an
168
+ // interleaved frame is a connection error precisely because the HPACK
169
+ // decoder is connection-wide and would be fed the fragments out of order.
170
+ var continuationStream = 0;
171
+ var continuationFragments = [];
172
+ var continuationFrame = null;
173
+
174
+
175
+ function connectionError(message, code) {
176
+ if (closed) return;
177
+ var err = message instanceof Error ? message
178
+ : F.h2Error(message, code === undefined ? F.ERROR.PROTOCOL_ERROR : code);
179
+ // A protocol error says what was wrong; anything else is ours (an
180
+ // exception from a listener, the application's request handler among
181
+ // them) and is INTERNAL_ERROR, with no detail. Its message went out as the
182
+ // GOAWAY's debug data — internal error text handed to the client — under
183
+ // PROTOCOL_ERROR, which blamed the peer.
184
+ if (err.h2Code) sendGoaway(err.h2Code, err.message);
185
+ else sendGoaway(F.ERROR.INTERNAL_ERROR, 'internal error');
186
+ closed = true;
187
+ emit('error', err);
188
+ }
189
+
190
+ // A peer that keeps sending on a stream we have reset gets a reset back each
191
+ // time (RFC 9113 §5.1), but only so many: answering an endless stream of
192
+ // frames with an endless stream of RST_STREAMs is a reflection amplifier
193
+ // pointed at ourselves. Past the limit the connection goes.
194
+ var MAX_RESETS_PER_STREAM = 8;
195
+
196
+ function streamError(streamId, code, message) {
197
+ var s = streams[streamId];
198
+
199
+ // Do NOT bail out because the stream is already closed. That was the bug
200
+ // h2spec §5.1.8/9/11/12 found: a DATA or HEADERS frame arriving after we
201
+ // reset a stream MUST be answered with STREAM_CLOSED, and returning here
202
+ // meant answering with nothing at all. The peer sat waiting for a reply
203
+ // that never came — every one of those four tests failed as "Timeout"
204
+ // rather than as a wrong error code, which is what silence looks like from
205
+ // the other end.
206
+ if (s) {
207
+ s.resetCount = (s.resetCount || 0) + 1;
208
+ if (s.resetCount > MAX_RESETS_PER_STREAM) {
209
+ connectionError('too many resets on stream ' + streamId, F.ERROR.ENHANCE_YOUR_CALM);
210
+ return;
211
+ }
212
+ // A stream the peer reset stays so: it is the peer's later frames that
213
+ // this answers.
214
+ if (s.closedBy !== 'rst_received') s.closedBy = 'rst_sent';
215
+ markClosed(s);
216
+ }
217
+
218
+ write(F.buildRstStream(streamId, code));
219
+ if (stats) stats.code(stats.streamResets.h2.sent, code);
220
+ emit('rst', streamId, code);
221
+ }
222
+
223
+ /**
224
+ * Has this stream id never been used, as opposed to used and finished?
225
+ *
226
+ * RFC 9113 §5.1 treats the two completely differently: a frame for an IDLE
227
+ * stream is a connection error, while the same frame for a CLOSED one is
228
+ * ordinary — the peer had it in flight when we closed the stream. Telling
229
+ * them apart is possible because ids only ever increase: anything above the
230
+ * highest we have seen was never opened, anything at or below it was.
231
+ */
232
+ function isIdleStreamId(id) {
233
+ if (streams[id]) return streams[id].state === STATE.IDLE;
234
+ var peerInitiated = isServer ? (id % 2 === 1) : (id % 2 === 0);
235
+ if (peerInitiated) return id > lastRemoteStreamId;
236
+ return id >= nextLocalStreamId;
237
+ }
238
+
239
+ function write(buffer) {
240
+ if (buffer && buffer.length) emit('send', buffer);
241
+ }
242
+
243
+
244
+ // ============================================================
245
+ // Streams
246
+ // ============================================================
247
+
248
+ function getStream(id, create) {
249
+ var s = streams[id];
250
+ if (s) return s;
251
+ if (!create) return null;
252
+ s = {
253
+ id: id,
254
+ state: STATE.IDLE,
255
+ // Per-stream windows, seeded from each side's INITIAL_WINDOW_SIZE.
256
+ localWindow: localSettings.INITIAL_WINDOW_SIZE,
257
+ remoteWindow: remoteSettings.INITIAL_WINDOW_SIZE,
258
+ headersReceived: false,
259
+ trailersReceived: false,
260
+ endStreamReceived: false,
261
+ endStreamSent: false,
262
+ pendingData: [], // queued while the send window is closed
263
+ contentLength: null,
264
+ bytesReceived: 0
265
+ };
266
+ streams[id] = s;
267
+ return s;
268
+ }
269
+
270
+ /**
271
+ * `how` records WHY the stream closed, because RFC 9113 §5.1 answers a later
272
+ * frame differently depending on it:
273
+ *
274
+ * 'rst_sent' — we reset it. What the peer sent before our RST_STREAM
275
+ * reached it is still arriving — a response's DATA up to
276
+ * a whole stream window — and §5.1 says to ignore it:
277
+ * "An endpoint MUST ignore frames that it receives on
278
+ * closed streams after it has sent a RST_STREAM frame"
279
+ * (counted in the connection's flow control all the
280
+ * same, §6.9). Each was answered with another RST_STREAM,
281
+ * and past MAX_RESETS_PER_STREAM the connection went
282
+ * (ENHANCE_YOUR_CALM): a download the client cancelled
283
+ * took the client's whole connection down with it once
284
+ * more than 8 frames were in flight.
285
+ * 'rst_received' — the peer reset it, so it knows the stream is gone: a
286
+ * frame after that is answered STREAM_CLOSED, a stream
287
+ * error (h2spec §5.1.9–11).
288
+ * 'end' — END_STREAM was seen. Both sides agreed the stream was over, so a
289
+ * further frame means the peer's state machine has diverged from
290
+ * ours, and §5.1 makes that a CONNECTION error of type
291
+ * STREAM_CLOSED.
292
+ *
293
+ * The two look identical from the outside — the stream is closed either way
294
+ * — which is why h2spec §5.1.12 was the last test standing: we answered a
295
+ * post-END_STREAM HEADERS with a polite RST_STREAM where the spec requires
296
+ * the connection to end.
297
+ */
298
+ function closeStream(id, how) {
299
+ var s = streams[id];
300
+ if (!s) return;
301
+ if (how) s.closedBy = how;
302
+ markClosed(s);
303
+ }
304
+
305
+ function markOpen(s) {
306
+ s.state = STATE.OPEN;
307
+ if (!s.active) { s.active = true; activeCount++; }
308
+ }
309
+
310
+ // Closed streams are kept briefly rather than deleted: frames for a
311
+ // just-closed stream are ordinary (the peer had them in flight when we
312
+ // closed it) and must be ignored, not treated as frames for an idle stream
313
+ // — which would be a connection error. Kept in the order they closed, and
314
+ // let go after 30 s or beyond the most recent MAX_RETAINED_CLOSED. The sweep
315
+ // walked every record on every close once there were 1000 of them, and kept
316
+ // everything closed in the last 30 s however many that was — a peer that
317
+ // opened and reset streams as fast as it could grew the map without bound
318
+ // and made each close slower than the last.
319
+ var MAX_RETAINED_CLOSED = 4096;
320
+ var closedOrder = [];
321
+ function markClosed(s) {
322
+ s.state = STATE.CLOSED;
323
+ s.pendingData = [];
324
+ waiting.delete(s);
325
+ if (s.active) { s.active = false; activeCount--; emit('stream_close', s.id); }
326
+ if (s.closedAt) return;
327
+ s.closedAt = Date.now();
328
+ closedOrder.push(s.id);
329
+ var cutoff = s.closedAt - 30000;
330
+ while (closedOrder.length > 0) {
331
+ var old = streams[closedOrder[0]];
332
+ if (old && old.state === STATE.CLOSED && closedOrder.length <= MAX_RETAINED_CLOSED && old.closedAt >= cutoff) break;
333
+ if (old && old.state === STATE.CLOSED) delete streams[closedOrder[0]];
334
+ closedOrder.shift();
335
+ }
336
+ }
337
+
338
+
339
+ // ============================================================
340
+ // Receiving
341
+ // ============================================================
342
+
343
+ function execute(chunk) {
344
+ if (closed) return;
345
+ if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
346
+ buf = buf.length === 0 ? chunk : Buffer.concat([buf, chunk]);
347
+
348
+ try {
349
+ if (!prefaceReceived) {
350
+ if (buf.length < F.PREFACE.length) return;
351
+ if (!buf.subarray(0, F.PREFACE.length).equals(F.PREFACE)) {
352
+ // Almost always an HTTP/1.1 client that reached an h2-only port. The
353
+ // preface exists so this fails here instead of being parsed as a
354
+ // request line.
355
+ throw F.h2Error('invalid connection preface', F.ERROR.PROTOCOL_ERROR);
356
+ }
357
+ buf = buf.subarray(F.PREFACE.length);
358
+ prefaceReceived = true;
359
+ }
360
+
361
+ // Every frame is handled once. A STREAM error — thrown by the parser
362
+ // for a frame it refused, or by a handler — resets that stream and the
363
+ // loop goes on from the next frame. It used to end the loop with the
364
+ // buffer not advanced: every frame of the chunk already handled was
365
+ // handled again on the next read (a HEADERS block decoded twice, the
366
+ // HPACK table desynchronised), and the buffer only grew.
367
+ var offset = 0;
368
+ for (;;) {
369
+ var frame;
370
+ try {
371
+ frame = F.readFrame(buf, offset, localSettings.MAX_FRAME_SIZE);
372
+ } catch (err) {
373
+ if (!err.streamId || !err.consumed) throw err;
374
+ offset += err.consumed;
375
+ streamError(err.streamId, err.h2Code, err.message);
376
+ if (closed) return;
377
+ continue;
378
+ }
379
+ if (!frame) break;
380
+ offset += frame.consumed;
381
+ try {
382
+ handleFrame(frame);
383
+ } catch (err) {
384
+ if (!err.streamId) throw err;
385
+ streamError(err.streamId, err.h2Code, err.message);
386
+ }
387
+ if (closed) return;
388
+ }
389
+ buf = offset > 0 ? buf.subarray(offset) : buf;
390
+
391
+ } catch (err) {
392
+ connectionError(err);
393
+ }
394
+ }
395
+
396
+
397
+ function handleFrame(frame) {
398
+ // §6.10 again: while a header block is open, the ONLY frame that may
399
+ // arrive is a CONTINUATION for the same stream.
400
+ if (continuationStream !== 0) {
401
+ if (frame.type !== F.TYPE.CONTINUATION || frame.streamId !== continuationStream) {
402
+ throw F.h2Error('frame ' + frame.typeName + ' interleaved into a header block',
403
+ F.ERROR.PROTOCOL_ERROR);
404
+ }
405
+ }
406
+
407
+ switch (frame.type) {
408
+ case F.TYPE.SETTINGS: return onSettings(frame);
409
+ case F.TYPE.HEADERS: return onHeaders(frame);
410
+ case F.TYPE.CONTINUATION: return onContinuation(frame);
411
+ case F.TYPE.DATA: return onData(frame);
412
+ case F.TYPE.RST_STREAM: return onRstStream(frame);
413
+ case F.TYPE.WINDOW_UPDATE: return onWindowUpdate(frame);
414
+ case F.TYPE.PING: return onPing(frame);
415
+ case F.TYPE.GOAWAY: return onGoaway(frame);
416
+ case F.TYPE.PRIORITY: return; // parsed and ignored; deprecated by RFC 9113
417
+ case F.TYPE.PRIORITY_UPDATE: return onPriorityUpdate(frame);
418
+ case F.TYPE.PUSH_PROMISE:
419
+ // We advertise ENABLE_PUSH 0, so receiving one is a protocol error
420
+ // (§6.6). A server receiving PUSH_PROMISE is always an error.
421
+ throw F.h2Error('PUSH_PROMISE received although push is disabled',
422
+ F.ERROR.PROTOCOL_ERROR);
423
+ default:
424
+ return; // unknown type: ignore (§4.1)
425
+ }
426
+ }
427
+
428
+
429
+ function onSettings(frame) {
430
+ if (frame.ack) { settingsAcked = true; return; }
431
+
432
+ for (var name in frame.settings) {
433
+ var value = frame.settings[name];
434
+
435
+ if (name === 'INITIAL_WINDOW_SIZE') {
436
+ // §6.9.2: changing this retroactively adjusts the send window of every
437
+ // OPEN stream by the difference. Not applying the delta is a slow,
438
+ // baffling bug — streams stall or over-send long after the SETTINGS
439
+ // frame that caused it.
440
+ var delta = value - remoteSettings.INITIAL_WINDOW_SIZE;
441
+ for (var id in streams) {
442
+ var s = streams[id];
443
+ if (s.state === STATE.CLOSED) continue;
444
+ s.remoteWindow += delta;
445
+ if (s.remoteWindow > F.MAX_WINDOW) {
446
+ throw F.h2Error('INITIAL_WINDOW_SIZE change overflows a stream window',
447
+ F.ERROR.FLOW_CONTROL_ERROR);
448
+ }
449
+ }
450
+ }
451
+
452
+ if (name === 'ENABLE_CONNECT_PROTOCOL' &&
453
+ remoteSettings.ENABLE_CONNECT_PROTOCOL === 1 && value === 0) {
454
+ // §3: "a sender MUST NOT send a value of 0 after previously sending a
455
+ // value of 1". A stream may already be running extended CONNECT, and
456
+ // withdrawing permission would leave it in a state neither side can
457
+ // describe.
458
+ throw F.h2Error('SETTINGS_ENABLE_CONNECT_PROTOCOL cannot be withdrawn',
459
+ F.ERROR.PROTOCOL_ERROR);
460
+ }
461
+
462
+ remoteSettings[name] = value;
463
+
464
+ if (name === 'HEADER_TABLE_SIZE') encoder.setMaxTableSize(value);
465
+ }
466
+
467
+ // The ACK must go out before anything that relies on the new settings.
468
+ write(F.buildSettings(null, true));
469
+ // Both forms: the object for consumers that want a value by name, and the
470
+ // ordered list for fingerprinting, which needs the sequence as sent.
471
+ emit('settings', frame.settings, frame.settingsList || []);
472
+
473
+ // Flush anything that was waiting on a window this SETTINGS may have opened.
474
+ flushAll();
475
+ }
476
+
477
+
478
+ function onHeaders(frame) {
479
+ var id = frame.streamId;
480
+
481
+ if (isServer) {
482
+ // §5.1.1: client-initiated streams are odd, and ids must strictly
483
+ // increase. A reused or lower id would let a peer resurrect a closed
484
+ // stream, which is how request state gets confused between them.
485
+ if (id % 2 === 0) {
486
+ throw F.h2Error('client-initiated stream with an even id', F.ERROR.PROTOCOL_ERROR);
487
+ }
488
+ if (id <= lastRemoteStreamId && !streams[id]) {
489
+ throw F.h2Error('stream id ' + id + ' is not greater than the previous ' +
490
+ lastRemoteStreamId, F.ERROR.PROTOCOL_ERROR);
491
+ }
492
+ }
493
+
494
+ // A closed stream that has already been reaped leaves no record. Without
495
+ // one we cannot know whether it ended by RST_STREAM or by END_STREAM, and
496
+ // the stricter reading is the safe one: a peer opening a stream id it has
497
+ // already used is desynchronised whichever way it closed.
498
+ if (!streams[id] && !isIdleStreamId(id)) {
499
+ throw F.h2Error('HEADERS on a closed stream', F.ERROR.STREAM_CLOSED);
500
+ }
501
+
502
+ var streamIsNew = !streams[id];
503
+ var s = getStream(id, true);
504
+
505
+ // Every header block is decoded, whatever becomes of its stream — refused,
506
+ // reset, or after our GOAWAY. The HPACK table is shared by the connection
507
+ // and the peer's encoder has already applied this block to its copy; a
508
+ // block skipped here leaves the two tables apart, and every later block on
509
+ // every stream decodes wrong (RFC 9113 §4.3). A refused stream had its
510
+ // block skipped, and, when it lacked END_HEADERS, its CONTINUATION then
511
+ // found no open block and took the connection down. startHeaderBlock on a
512
+ // stream already closed decodes and drops (finishHeaderBlock).
513
+
514
+ // A new stream, announced before its request is processed — the point at
515
+ // which a rate limiter can refuse it: refused here, it costs the block's
516
+ // decode and never becomes a request. Guarded: no listener, no cost.
517
+ if (streamIsNew && isServer && ev.listenerCount('stream_open')) {
518
+ emit('stream_open', id);
519
+ if (s.state === STATE.CLOSED) {
520
+ // Refused by the listener (sendRstStream), which has already sent the
521
+ // RST; the id is used all the same.
522
+ lastRemoteStreamId = Math.max(lastRemoteStreamId, id);
523
+ startHeaderBlock(frame, id);
524
+ return;
525
+ }
526
+ }
527
+
528
+ if (s.state === STATE.CLOSED) {
529
+ if (s.closedBy === 'rst_sent') {
530
+ // Reset by us: in flight before our RST_STREAM — ignored, its block
531
+ // decoded all the same (HPACK's table is the connection's).
532
+ startHeaderBlock(frame, id);
533
+ } else if (s.closedBy === 'rst_received') {
534
+ streamError(id, F.ERROR.STREAM_CLOSED, 'HEADERS after RST_STREAM');
535
+ startHeaderBlock(frame, id);
536
+ } else {
537
+ throw F.h2Error('HEADERS after END_STREAM', F.ERROR.STREAM_CLOSED);
538
+ }
539
+ return;
540
+ }
541
+ if (s.state === STATE.HALF_CLOSED_REMOTE) {
542
+ streamError(id, F.ERROR.STREAM_CLOSED, 'HEADERS after END_STREAM');
543
+ startHeaderBlock(frame, id);
544
+ return;
545
+ }
546
+
547
+ if (s.state === STATE.IDLE) {
548
+ // Refused streams use their id too: ids only increase (§5.1.1), and a
549
+ // refusal left lastRemoteStreamId behind, so a lower unused id was
550
+ // still accepted afterwards.
551
+ if (isServer) lastRemoteStreamId = id;
552
+
553
+ // After our GOAWAY, a stream above its last-stream-id is not processed
554
+ // (§6.8). It was: goaway() — which a rate limiter calls to stop a peer —
555
+ // did not stop new requests at all. REFUSED_STREAM tells the client it
556
+ // may retry the request on a new connection.
557
+ if (isServer && goawaySent && id > goawayLastStreamId) {
558
+ streamError(id, F.ERROR.REFUSED_STREAM, 'received after GOAWAY');
559
+ startHeaderBlock(frame, id);
560
+ return;
561
+ }
562
+ if (isServer && activeCount >= localSettings.MAX_CONCURRENT_STREAMS) {
563
+ // §5.1.2: refusing is a stream error, and REFUSED_STREAM specifically
564
+ // tells the peer the request was never processed, so it is safe to
565
+ // retry elsewhere.
566
+ streamError(id, F.ERROR.REFUSED_STREAM, 'too many concurrent streams');
567
+ startHeaderBlock(frame, id);
568
+ return;
569
+ }
570
+ markOpen(s);
571
+ }
572
+
573
+ startHeaderBlock(frame, id);
574
+ }
575
+
576
+ // A header block (HEADERS + CONTINUATION) is bounded while it accumulates,
577
+ // before any of it can be decoded: in bytes, and in frames. Nothing bounded
578
+ // it — fragments were collected until END_HEADERS, however many and however
579
+ // large, including empty ones (the CONTINUATION flood of 2024). The byte
580
+ // bound leaves room for the decoded list's own limit (MAX_HEADER_LIST_SIZE)
581
+ // even through HPACK's worst-case expansion; the frame bound is nghttp2's,
582
+ // and so node's. Past either, the block cannot be decoded and the HPACK
583
+ // state is lost with it: a connection error.
584
+ var MAX_CONTINUATION_FRAMES = 8;
585
+ var continuationBytes = 0;
586
+ var continuationCount = 0;
587
+ function headerBlockByteLimit() {
588
+ return Math.max(64 * 1024, 4 * localSettings.MAX_HEADER_LIST_SIZE);
589
+ }
590
+
591
+ function onContinuation(frame) {
592
+ if (continuationStream === 0) {
593
+ throw F.h2Error('CONTINUATION with no open header block', F.ERROR.PROTOCOL_ERROR);
594
+ }
595
+ continuationBytes += frame.headerBlock.length;
596
+ if (++continuationCount > MAX_CONTINUATION_FRAMES) {
597
+ throw F.h2Error('header block split across too many CONTINUATION frames', F.ERROR.ENHANCE_YOUR_CALM);
598
+ }
599
+ if (continuationBytes > headerBlockByteLimit()) {
600
+ throw F.h2Error('header block too large', F.ERROR.ENHANCE_YOUR_CALM);
601
+ }
602
+ continuationFragments.push(frame.headerBlock);
603
+ if (frame.endHeaders) finishHeaderBlock();
604
+ }
605
+
606
+ // A header block left open — HEADERS without END_HEADERS — holds the whole
607
+ // connection: until its CONTINUATIONs end it, no other frame may arrive
608
+ // (§6.10). 'header_block_open' and 'header_block_closed' bracket that time,
609
+ // for whoever bounds it (h2_server's headersTimeout); refused and reset
610
+ // streams' blocks included, since they hold the connection just the same.
611
+ var headerBlockOpen = false;
612
+
613
+ function startHeaderBlock(frame, id) {
614
+ continuationStream = id;
615
+ continuationFragments = [frame.headerBlock];
616
+ continuationFrame = frame;
617
+ continuationBytes = frame.headerBlock.length;
618
+ continuationCount = 0;
619
+ if (frame.endHeaders) { finishHeaderBlock(); return; }
620
+ headerBlockOpen = true;
621
+ emit('header_block_open', id);
622
+ }
623
+
624
+ function finishHeaderBlock() {
625
+ var id = continuationStream;
626
+ var frame = continuationFrame;
627
+ var block = Buffer.concat(continuationFragments);
628
+
629
+ continuationStream = 0;
630
+ continuationFragments = [];
631
+ continuationFrame = null;
632
+ if (headerBlockOpen) { headerBlockOpen = false; emit('header_block_closed', id); }
633
+
634
+ var list;
635
+ try {
636
+ list = decoder.decode(block);
637
+ } catch (err) {
638
+ // HPACK failures are ALWAYS connection errors: the table is shared, so a
639
+ // block we could not decode leaves our table and the peer's permanently
640
+ // out of step, and every later block on every stream would be wrong.
641
+ throw F.h2Error(err.message, F.ERROR.COMPRESSION_ERROR);
642
+ }
643
+
644
+ var s = streams[id];
645
+ if (!s || s.state === STATE.CLOSED) return; // reset while the block arrived
646
+
647
+ var headers;
648
+ try {
649
+ headers = validateHeaderList(list, s);
650
+ } catch (err) {
651
+ streamError(id, err.h2Code || F.ERROR.PROTOCOL_ERROR, err.message);
652
+ return;
653
+ }
654
+
655
+ if (s.headersReceived) {
656
+ // A second header block on the same stream is trailers, and trailers
657
+ // must end the stream (§8.1).
658
+ if (!frame.endStream) {
659
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'trailers without END_STREAM');
660
+ return;
661
+ }
662
+ // A body that content-length promised and the trailers ended short
663
+ // (§8.1.1) — checked on DATA's END_STREAM, not here.
664
+ if (isServer && s.contentLength !== null && s.bytesReceived !== s.contentLength) {
665
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'body shorter than content-length');
666
+ return;
667
+ }
668
+ s.trailersReceived = true;
669
+ emit('trailers', id, headers);
670
+ } else if (!isServer && /^1\d\d$/.test(String(headers[':status']))) {
671
+ // A 1xx response (103 Early Hints) is not the response (§8.1): the
672
+ // final head follows on the same stream. Counted as the head, the
673
+ // real one after it was taken for trailers — with a :status in it,
674
+ // malformed — and the stream was reset: a response with Early Hints
675
+ // never arrived. It may not end the stream.
676
+ if (frame.endStream) {
677
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'informational response with END_STREAM');
678
+ return;
679
+ }
680
+ emit('headers', id, headers, false, null);
681
+ return;
682
+ } else {
683
+ s.headersReceived = true;
684
+ try {
685
+ var n = parseContentLength(headers['content-length']);
686
+ if (n !== null) s.contentLength = n;
687
+ } catch (e) {
688
+ streamError(id, F.ERROR.PROTOCOL_ERROR, e.message);
689
+ return;
690
+ }
691
+ // A request that promises a body and ends with its headers (§8.1.1).
692
+ // Requests only: a response to HEAD, or a 304, carries a length and no body.
693
+ if (isServer && frame.endStream && s.contentLength) {
694
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'content-length ' + s.contentLength + ' but no body');
695
+ return;
696
+ }
697
+ if (isServer) applyRequestPriority(s, headers);
698
+ // An extended CONNECT stream is a bidirectional byte channel, not a
699
+ // request/response pair: END_STREAM on the request would close the
700
+ // client's half before the protocol on top has said anything.
701
+ emit('headers', id, headers, !!frame.endStream, s.extendedConnect || null);
702
+ }
703
+
704
+ if (frame.endStream) receiveEndStream(s);
705
+ }
706
+
707
+
708
+ /**
709
+ * §8.3 and §8.2.2. Malformed here means a STREAM error, not a connection
710
+ * error: one bad request should not take down the other streams sharing the
711
+ * connection.
712
+ */
713
+ function validateHeaderList(list, stream) {
714
+ // The rules are shared with HTTP/3 (validateFieldSection, http_common);
715
+ // what is HTTP/2's is the answer — a stream error, PROTOCOL_ERROR — and
716
+ // where the result goes.
717
+ var r;
718
+ try {
719
+ r = validateFieldSection(list, {
720
+ isServer: isServer,
721
+ trailers: stream.headersReceived,
722
+ connectProtocol: !!localSettings.ENABLE_CONNECT_PROTOCOL
723
+ });
724
+ } catch (err) {
725
+ throw F.h2Error(err.message, F.ERROR.PROTOCOL_ERROR);
726
+ }
727
+ if (r.protocol) stream.extendedConnect = r.protocol;
728
+ // Emitted only after validation passes: a malformed header list is not a
729
+ // fingerprint, and reporting one would let a peer feed noise into whatever
730
+ // is collecting these.
731
+ if (r.pseudoOrder.length) emit('pseudo_order', r.pseudoOrder);
732
+ return r.headers;
733
+ }
734
+
735
+
736
+ function onData(frame) {
737
+ var id = frame.streamId;
738
+ var s = streams[id];
739
+
740
+ // Flow control is accounted for even on a stream we are about to reject:
741
+ // the peer has spent the window, and if we do not give it back the
742
+ // connection stalls (§6.9.1). This is the classic "works until it
743
+ // doesn't" bug — small bodies pass, then everything freezes.
744
+ var len = frame.length;
745
+ localWindow -= len;
746
+ if (localWindow < 0) {
747
+ throw F.h2Error('connection flow-control window exceeded', F.ERROR.FLOW_CONTROL_ERROR);
748
+ }
749
+ maybeSendConnectionWindowUpdate(len);
750
+
751
+ if (isIdleStreamId(id)) {
752
+ throw F.h2Error('DATA on an idle stream', F.ERROR.PROTOCOL_ERROR);
753
+ }
754
+ if (!s || s.state === STATE.CLOSED || s.state === STATE.HALF_CLOSED_REMOTE) {
755
+ // DATA after END_STREAM is a connection error for the same reason as
756
+ // HEADERS; after a reset it is a stream error. §5.1 again.
757
+ if (s && s.closedBy === 'rst_sent') {
758
+ // In flight before our RST_STREAM: ignored (counted above).
759
+ } else if (s && s.closedBy === 'rst_received') {
760
+ streamError(id, F.ERROR.STREAM_CLOSED, 'DATA after RST_STREAM');
761
+ } else {
762
+ throw F.h2Error('DATA after END_STREAM', F.ERROR.STREAM_CLOSED);
763
+ }
764
+ return;
765
+ }
766
+
767
+ s.localWindow -= len;
768
+ if (s.localWindow < 0) {
769
+ streamError(id, F.ERROR.FLOW_CONTROL_ERROR, 'stream window exceeded');
770
+ return;
771
+ }
772
+
773
+ s.bytesReceived += frame.data.length;
774
+ if (s.contentLength !== null && s.bytesReceived > s.contentLength) {
775
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'body longer than content-length');
776
+ return;
777
+ }
778
+
779
+ if (frame.data.length) emit('data', id, frame.data);
780
+ maybeSendStreamWindowUpdate(s, len);
781
+
782
+ if (frame.endStream) {
783
+ if (s.contentLength !== null && s.bytesReceived !== s.contentLength) {
784
+ streamError(id, F.ERROR.PROTOCOL_ERROR, 'body shorter than content-length');
785
+ return;
786
+ }
787
+ receiveEndStream(s);
788
+ }
789
+ }
790
+
791
+ function receiveEndStream(s) {
792
+ s.endStreamReceived = true;
793
+ if (s.state === STATE.OPEN) s.state = STATE.HALF_CLOSED_REMOTE;
794
+ else if (s.state === STATE.HALF_CLOSED_LOCAL) closeStream(s.id, 'end');
795
+ emit('end', s.id);
796
+ }
797
+
798
+
799
+ function onRstStream(frame) {
800
+ if (isIdleStreamId(frame.streamId)) {
801
+ throw F.h2Error('RST_STREAM on an idle stream', F.ERROR.PROTOCOL_ERROR);
802
+ }
803
+ closeStream(frame.streamId, 'rst_received');
804
+ if (stats) stats.code(stats.streamResets.h2.received, frame.errorCode);
805
+ emit('rst', frame.streamId, frame.errorCode);
806
+
807
+ // Separate from 'rst', which also fires for resets WE send. Only the
808
+ // peer's resets are the Rapid Reset signal (CVE-2023-44487): open a
809
+ // stream, reset it immediately, repeat. MAX_CONCURRENT_STREAMS never
810
+ // catches it because no stream stays open — the rate of resets is the
811
+ // only thing that shows it.
812
+ if (ev.listenerCount('stream_reset')) {
813
+ emit('stream_reset', frame.streamId, frame.errorCode);
814
+ }
815
+ }
816
+
817
+
818
+ function onWindowUpdate(frame) {
819
+ // Surfaced before any validation: the connection-level increment a peer
820
+ // opens with is one of the strongest implementation tells there is —
821
+ // Chrome sends 15663105, Firefox 12517377, and most libraries send none.
822
+ emit('window_update', frame.streamId, frame.increment);
823
+
824
+ if (frame.streamId === 0) {
825
+ remoteWindow += frame.increment;
826
+ if (remoteWindow > F.MAX_WINDOW) {
827
+ throw F.h2Error('connection send window overflow', F.ERROR.FLOW_CONTROL_ERROR);
828
+ }
829
+ flushAll();
830
+ return;
831
+ }
832
+
833
+ if (isIdleStreamId(frame.streamId)) {
834
+ // §5.1: nothing may be sent for a stream that was never opened. This is
835
+ // the distinction the old code missed — it lumped "idle" together with
836
+ // "closed" and ignored both, so h2spec §5.1.3 waited for a GOAWAY that
837
+ // never came.
838
+ throw F.h2Error('WINDOW_UPDATE on an idle stream', F.ERROR.PROTOCOL_ERROR);
839
+ }
840
+
841
+ var s = streams[frame.streamId];
842
+ if (!s || s.state === STATE.CLOSED) {
843
+ // Genuinely closed: the peer had this in flight when we closed the
844
+ // stream, which is ordinary and must be ignored (§6.9).
845
+ return;
846
+ }
847
+ s.remoteWindow += frame.increment;
848
+ if (s.remoteWindow > F.MAX_WINDOW) {
849
+ streamError(frame.streamId, F.ERROR.FLOW_CONTROL_ERROR, 'stream send window overflow');
850
+ return;
851
+ }
852
+ flushStream(s);
853
+ }
854
+
855
+
856
+ function onPing(frame) {
857
+ if (frame.ack) { emit('pong', frame.payload); return; }
858
+ // §6.7: a PING must be answered immediately with the same payload. It is
859
+ // also how a peer measures RTT, so delaying it is not neutral.
860
+ write(F.buildPing(frame.payload, true));
861
+ emit('ping', frame.payload);
862
+ }
863
+
864
+
865
+ function onGoaway(frame) {
866
+ goawayReceived = true;
867
+ if (stats) stats.code(stats.closes.h2.received, frame.errorCode);
868
+ emit('goaway', frame.lastStreamId, frame.errorCode, frame.debugData);
869
+ }
870
+
871
+
872
+ // ============================================================
873
+ // Flow control — receive side
874
+ //
875
+ // When credit is returned decides whether flow control does anything at all.
876
+ //
877
+ // autoWindowUpdate (the default) grants it as soon as a DATA frame is
878
+ // parsed. Simple, and correct for a consumer that keeps up — but it means
879
+ // the window never actually closes, so there is no back-pressure: a peer
880
+ // can push a body far faster than the application reads it and the excess
881
+ // piles up in our memory. That is fine for a proxy that forwards
882
+ // immediately and wrong for anything that buffers.
883
+ //
884
+ // With autoWindowUpdate false, credit is returned only when the consumer
885
+ // calls consume(streamId, bytes). The window then reflects what has really
886
+ // been dealt with, and a slow application slows the peer down instead of
887
+ // filling the heap. h2_server uses this so that a paused Readable
888
+ // back-pressures the network, the same way h1_server lets TCP do it.
889
+ //
890
+ // The threshold exists because a WINDOW_UPDATE per DATA frame doubles the
891
+ // frame count for no benefit; half the window is what every implementation
892
+ // uses.
893
+ // ============================================================
894
+
895
+ var autoWindowUpdate = options.autoWindowUpdate !== false;
896
+ var creditNow = autoWindowUpdate;
897
+ var pendingConnectionCredit = 0;
898
+
899
+ function maybeSendConnectionWindowUpdate(consumed) {
900
+ pendingConnectionCredit += consumed;
901
+ if (!creditNow) return;
902
+ // Keyed off the connection window's own size, for the same reason as
903
+ // above: with a small INITIAL_WINDOW_SIZE this would otherwise emit a
904
+ // WINDOW_UPDATE for stream 0 after every few bytes. Its own size is
905
+ // CONNECTION_WINDOW (sendPreface widens it to that), not the 65535 it
906
+ // starts at: keyed off that, it went every 32 KB of a 16 MB window — a
907
+ // frame, a TLS record and a system call per 32 KB, 3000 of them in a
908
+ // 96 MB download, 10% of the client's CPU.
909
+ var threshold = Math.max(1, Math.floor(Math.max(F.DEFAULT_WINDOW_SIZE, CONNECTION_WINDOW) / 2));
910
+ if (pendingConnectionCredit >= threshold) {
911
+ write(F.buildWindowUpdate(0, pendingConnectionCredit));
912
+ localWindow += pendingConnectionCredit;
913
+ pendingConnectionCredit = 0;
914
+ }
915
+ }
916
+
917
+ function maybeSendStreamWindowUpdate(s, consumed) {
918
+ s.pendingCredit = (s.pendingCredit || 0) + consumed;
919
+ if (!creditNow) return;
920
+ var threshold = Math.max(1, Math.floor(localSettings.INITIAL_WINDOW_SIZE / 2));
921
+ if (s.pendingCredit >= threshold && s.state !== STATE.CLOSED) {
922
+ write(F.buildWindowUpdate(s.id, s.pendingCredit));
923
+ s.localWindow += s.pendingCredit;
924
+ s.pendingCredit = 0;
925
+ }
926
+ }
927
+
928
+
929
+ // ============================================================
930
+ // Sending
931
+ // ============================================================
932
+
933
+ function sendPreface() {
934
+ if (!isServer) write(F.PREFACE);
935
+
936
+ var toSend = {};
937
+ for (var k in localSettings) {
938
+ // A client has no use for ENABLE_CONNECT_PROTOCOL: it is a server
939
+ // telling clients what they may ask for, and RFC 8441 §3 only defines
940
+ // the server-to-client direction.
941
+ if (k === 'ENABLE_CONNECT_PROTOCOL' && !isServer) continue;
942
+ toSend[k] = localSettings[k];
943
+ }
944
+ write(F.buildSettings(toSend, false));
945
+
946
+ // Widen the CONNECTION receive window immediately.
947
+ //
948
+ // It starts at 65535 and, unlike a stream window, SETTINGS cannot change
949
+ // it — RFC 9113 §6.9.2 leaves WINDOW_UPDATE on stream 0 as the only way to
950
+ // move it. A server that never sends one is telling every peer it can
951
+ // accept 64KB across the whole connection before stopping to wait for
952
+ // credit, no matter how many streams are open or how large the per-stream
953
+ // windows are.
954
+ //
955
+ // Every real implementation widens it in its first flight, which is why
956
+ // Chrome's HTTP/2 session table shows send windows in the megabytes for
957
+ // other servers and exactly 65535 for one that does not. On a fast link
958
+ // the difference is a round trip per 64KB — the connection spends most of
959
+ // its time waiting for WINDOW_UPDATEs it could have been given up front.
960
+ //
961
+ // 16MB nearly matches what Chrome itself advertises (15728640) and is
962
+ // comfortably under the 2^31-1 ceiling.
963
+ var bump = CONNECTION_WINDOW - F.DEFAULT_WINDOW_SIZE;
964
+ if (bump > 0) {
965
+ localWindow += bump;
966
+ write(F.buildWindowUpdate(0, bump));
967
+ }
968
+ }
969
+
970
+ function sendHeaders(streamId, headers, endStream) {
971
+ var s = getStream(streamId, true);
972
+ if (s.state === STATE.IDLE) markOpen(s);
973
+
974
+ var block = encoder.encode(headers);
975
+ var frames = F.buildHeaderFrames(streamId, block, { endStream: !!endStream },
976
+ remoteSettings.MAX_FRAME_SIZE);
977
+ // Concatenated into ONE write: §6.10 forbids any other frame between
978
+ // HEADERS and its CONTINUATIONs, and emitting them separately invites a
979
+ // caller (or a queue) to interleave.
980
+ write(Buffer.concat(frames));
981
+
982
+ if (endStream) sendEndStream(s);
983
+ }
984
+
985
+ /**
986
+ * Queue body bytes. Returns false when the send window is closed, so a
987
+ * caller can stop producing — the same contract as a Node stream's write().
988
+ */
989
+ function sendData(streamId, data, endStream) {
990
+ var s = streams[streamId];
991
+ if (!s || s.state === STATE.CLOSED) return false;
992
+ // The caller's bytes as they are, not a copy — as net.Socket takes them:
993
+ // what is handed to write() is not the caller's to change until it has
994
+ // gone. A copy here, and another to put a frame header in front of each
995
+ // piece, were most of what a large response cost.
996
+ if (data && data.length) {
997
+ s.pendingData.push(typeof data === 'string' ? Buffer.from(data)
998
+ : Buffer.isBuffer(data) ? data
999
+ : Buffer.from(data.buffer, data.byteOffset, data.byteLength));
1000
+ }
1001
+ if (endStream) s.pendingEnd = true;
1002
+ // Straight out while nothing else waits; behind the others, in their
1003
+ // order, when something does (see flushAll).
1004
+ if (waiting.size === 0 || (waiting.size === 1 && waiting.has(s))) flushStream(s);
1005
+ else { waiting.add(s); flushAll(); }
1006
+ // Done with this write once nothing of it is left queued; 'drain' follows
1007
+ // when what is queued has gone. It returned whether the window was still
1008
+ // open — so a write that used it up exactly, leaving nothing queued, was
1009
+ // told to wait for a 'drain' that nothing would ever emit.
1010
+ return s.pendingData.length === 0;
1011
+ }
1012
+
1013
+ function sendableBytes(s) {
1014
+ return Math.max(0, Math.min(s.remoteWindow, remoteWindow));
1015
+ }
1016
+
1017
+ /**
1018
+ * One DATA frame of a stream's queue — as much as both windows allow, up to
1019
+ * MAX_FRAME_SIZE — or its END_STREAM alone once the queue is empty. True if
1020
+ * a frame was written.
1021
+ *
1022
+ * Both windows: the connection's and the stream's. Checking only one is the
1023
+ * single most common HTTP/2 flow-control bug — it works in every test with a
1024
+ * single stream and deadlocks the moment there are several, because one
1025
+ * greedy stream drains the connection window and the others never learn why
1026
+ * they stalled.
1027
+ */
1028
+ function writeFrame(s) {
1029
+ if (s.state === STATE.CLOSED) return false;
1030
+ if (s.pendingData.length === 0) {
1031
+ if (!s.pendingEnd) return false;
1032
+ s.pendingEnd = false;
1033
+ write(F.buildData(s.id, Buffer.alloc(0), true));
1034
+ sendEndStream(s);
1035
+ return true;
1036
+ }
1037
+ // The windows say what the PEER will accept; the transport's buffer says
1038
+ // what is actually leaving. A peer that grants a large window and then
1039
+ // stops reading had every byte its windows allowed — up to 2 GB a stream
1040
+ // — queued in the socket (the "internal data buffering" attack of 2019).
1041
+ // DATA now waits in this queue, and flushAll() resumes when the socket
1042
+ // drains.
1043
+ if (isWriteBlocked()) return false;
1044
+ var budget = Math.min(sendableBytes(s), remoteSettings.MAX_FRAME_SIZE);
1045
+ if (budget <= 0) { emit('window_blocked', s.id); return false; }
1046
+
1047
+ var chunk = s.pendingData[0];
1048
+ var take = Math.min(chunk.length, budget);
1049
+ var piece = chunk.subarray(0, take);
1050
+
1051
+ if (take === chunk.length) s.pendingData.shift();
1052
+ else s.pendingData[0] = chunk.subarray(take);
1053
+
1054
+ s.remoteWindow -= take;
1055
+ remoteWindow -= take;
1056
+
1057
+ var last = s.pendingData.length === 0 && s.pendingEnd;
1058
+ // Header and payload as two writes, the payload never copied; the
1059
+ // server corks the socket, so they still leave together.
1060
+ write(F.frameHeader(take, F.TYPE.DATA, last ? F.FLAG.END_STREAM : 0, s.id));
1061
+ write(piece);
1062
+ if (last) { s.pendingEnd = false; sendEndStream(s); }
1063
+ if (s.pendingData.length === 0) { waiting.delete(s); emit('drain', s.id); }
1064
+ return true;
1065
+ }
1066
+
1067
+ // As much of one stream's queue as can go now; what cannot waits (waiting).
1068
+ function flushStream(s) {
1069
+ if (!s) return;
1070
+ while (writeFrame(s)) {}
1071
+ if (s.state !== STATE.CLOSED && (s.pendingData.length > 0 || s.pendingEnd)) waiting.add(s);
1072
+ else waiting.delete(s);
1073
+ }
1074
+
1075
+ // ---- Which stream's DATA goes next (RFC 9218) ────────────────────────
1076
+ //
1077
+ // The streams whose DATA waits — for a window, or for the socket to drain —
1078
+ // and the order they go in when there is room, by the rules HTTP/3's
1079
+ // sender uses (quic_connection's execute_quic_burst): urgency first, most
1080
+ // urgent first; within one, the non-incremental streams one at a time, by
1081
+ // stream number, then the incremental ones taking turns, a frame each,
1082
+ // the round starting after whoever went first last time. A stream with no
1083
+ // signal takes turns at urgency 3. They went by stream number, each until
1084
+ // blocked: after every drain the lowest-numbered stream took all the room.
1085
+ //
1086
+ // Only what waits is ordered: a write while nothing waits goes straight to
1087
+ // the socket, and what the socket holds (up to its mark, isWriteBlocked)
1088
+ // leaves in the order it was written.
1089
+ var waiting = new Set();
1090
+ var rrCursor = {}; // urgency → the stream that opened the last round
1091
+
1092
+ function flushAll() {
1093
+ if (waiting.size === 0) return;
1094
+ var tiers = new Map();
1095
+ waiting.forEach(function (s) {
1096
+ if (s.state === STATE.CLOSED) { waiting.delete(s); return; }
1097
+ var pr = s.priority, u = pr && pr.urgency != null ? pr.urgency : 3;
1098
+ var t = tiers.get(u);
1099
+ if (!t) { t = { seq: [], inc: [] }; tiers.set(u, t); }
1100
+ (pr && pr.incremental === false ? t.seq : t.inc).push(s);
1101
+ });
1102
+ var byId = function (a, b) { return a.id - b.id; };
1103
+ var urgencies = Array.from(tiers.keys()).sort(function (a, b) { return a - b; });
1104
+ for (var ui = 0; ui < urgencies.length; ui++) {
1105
+ var u = urgencies[ui], t = tiers.get(u);
1106
+ t.seq.sort(byId);
1107
+ for (var i = 0; i < t.seq.length; i++) {
1108
+ flushStream(t.seq[i]);
1109
+ if (isWriteBlocked()) return;
1110
+ }
1111
+ var inc = t.inc.sort(byId);
1112
+ if (inc.length === 0) continue;
1113
+ var first = 0;
1114
+ if (rrCursor[u] !== undefined) {
1115
+ while (first < inc.length && inc[first].id <= rrCursor[u]) first++;
1116
+ if (first === inc.length) first = 0;
1117
+ }
1118
+ var opened = false, progress = true;
1119
+ while (progress) {
1120
+ progress = false;
1121
+ for (var k = 0; k < inc.length; k++) {
1122
+ var st = inc[(first + k) % inc.length];
1123
+ if (!writeFrame(st)) continue;
1124
+ progress = true;
1125
+ if (!opened) { rrCursor[u] = st.id; opened = true; }
1126
+ }
1127
+ if (isWriteBlocked()) break;
1128
+ }
1129
+ for (var j = 0; j < inc.length; j++) flushStream(inc[j]); // waiting, as they now are
1130
+ if (isWriteBlocked()) return;
1131
+ }
1132
+ }
1133
+
1134
+ // ---- RFC 9218's signals ----
1135
+ //
1136
+ // As in HTTP/3 (h3.js): a request's `priority` header and PRIORITY_UPDATE,
1137
+ // which replaces it, are the client's signals; what the application sets
1138
+ // (setStreamPriority, h2_server's res.setPriority) is the server's decision,
1139
+ // and a signal does not override it (mergePriority). An update for a
1140
+ // stream not yet opened is held until its request arrives — bounded,
1141
+ // oldest out.
1142
+ var PRIORITY_PENDING_MAX = 128;
1143
+ var prioPending = new Map(); // stream id → Priority Field Value
1144
+
1145
+ function setStreamPriority(id, p, fromPeer) {
1146
+ var s = streams[id];
1147
+ if (!s || s.state === STATE.CLOSED) return false;
1148
+ s.priority = mergePriority(s.priority || {}, p, fromPeer);
1149
+ if (waiting.has(s)) flushAll();
1150
+ return true;
1151
+ }
1152
+
1153
+ function applyRequestPriority(s, headers) {
1154
+ if (headers.priority != null) setStreamPriority(s.id, parsePriority(headers.priority), true);
1155
+ if (prioPending.has(s.id)) {
1156
+ setStreamPriority(s.id, parsePriority(prioPending.get(s.id)), true);
1157
+ prioPending.delete(s.id);
1158
+ }
1159
+ }
1160
+
1161
+ function onPriorityUpdate(frame) {
1162
+ // §7.1: the server sends none; the stream must be one the client opens.
1163
+ if (!isServer) throw F.h2Error('PRIORITY_UPDATE from the server', F.ERROR.PROTOCOL_ERROR);
1164
+ var id = frame.prioritizedStreamId;
1165
+ if (id % 2 !== 1) throw F.h2Error('PRIORITY_UPDATE for stream ' + id + ', not a request stream', F.ERROR.PROTOCOL_ERROR);
1166
+ var s = streams[id];
1167
+ if (isIdleStreamId(id) || (s && !s.headersReceived)) {
1168
+ prioPending.delete(id);
1169
+ prioPending.set(id, frame.priorityField);
1170
+ if (prioPending.size > PRIORITY_PENDING_MAX) prioPending.delete(prioPending.keys().next().value);
1171
+ return;
1172
+ }
1173
+ setStreamPriority(id, parsePriority(frame.priorityField), true); // a closed stream: nothing to do
1174
+ }
1175
+
1176
+ function sendEndStream(s) {
1177
+ s.endStreamSent = true;
1178
+ if (s.state === STATE.OPEN) {
1179
+ s.state = STATE.HALF_CLOSED_LOCAL;
1180
+ // Our side is done and the peer's is not: a server that answered
1181
+ // before the request body was in (RFC 9113 §8.1). The layer above
1182
+ // decides whether to tell the client to stop (RST_STREAM NO_ERROR).
1183
+ emit('local_end', s.id);
1184
+ }
1185
+ else if (s.state === STATE.HALF_CLOSED_REMOTE) closeStream(s.id, 'end');
1186
+ }
1187
+
1188
+ function sendGoaway(errorCode, debug) {
1189
+ if (goawaySent) return;
1190
+ goawaySent = true;
1191
+ goawayLastStreamId = lastRemoteStreamId;
1192
+ write(F.buildGoaway(lastRemoteStreamId, errorCode || F.ERROR.NO_ERROR, debug));
1193
+ if (stats) stats.code(stats.closes.h2.sent, errorCode || F.ERROR.NO_ERROR);
1194
+ }
1195
+
1196
+
1197
+ // ============================================================
1198
+ // API
1199
+ // ============================================================
1200
+
1201
+ this.on = on;
1202
+ // off() matters here in a way it does not for a parser: h2_server registers
1203
+ // a one-shot 'drain' listener per blocked write, and a long response on a
1204
+ // narrow window blocks thousands of times. Without a way to remove them the
1205
+ // listener array grows for the life of the connection.
1206
+ this.off = off;
1207
+ this.execute = execute;
1208
+ this.sendPreface = sendPreface;
1209
+ this.sendHeaders = sendHeaders;
1210
+ this.sendData = sendData;
1211
+ /** The transport can take more again (its 'drain'): send what DATA waited. */
1212
+ this.flushAll = flushAll;
1213
+ /**
1214
+ * A stream's priority: { urgency (0..7, 0 first; default 3), incremental }
1215
+ * (RFC 9218). Set by the application, it is the decision; fromPeer marks
1216
+ * the peer's signal, which does not override it (see mergePriority).
1217
+ */
1218
+ this.setStreamPriority = setStreamPriority;
1219
+ /** The client's side: change a request's priority after it was sent (RFC 9218 §7.1). */
1220
+ this.sendPriorityUpdate = function (streamId, p) {
1221
+ if (isServer) return false;
1222
+ write(F.buildPriorityUpdate(Number(streamId), formatPriority(p)));
1223
+ return true;
1224
+ };
1225
+ this.sendGoaway = sendGoaway;
1226
+
1227
+ /**
1228
+ * Tell the peer to stop opening streams.
1229
+ *
1230
+ * NOT a disconnect: RFC 9113 §6.8 leaves streams below lastStreamId running
1231
+ * to completion, and only forbids new ones. That is what makes it right for
1232
+ * a rate limiter — in-flight requests still get answered — and wrong if the
1233
+ * intent is to cut the peer off, which needs destroying the socket as well.
1234
+ */
1235
+ this.goaway = function (errorCode) {
1236
+ sendGoaway(errorCode === undefined ? F.ERROR.NO_ERROR : errorCode);
1237
+ };
1238
+
1239
+ this.sendRstStream = function (streamId, errorCode) {
1240
+ streamError(streamId, errorCode === undefined ? F.ERROR.CANCEL : errorCode);
1241
+ };
1242
+
1243
+ this.sendPing = function (payload) { write(F.buildPing(payload, false)); };
1244
+
1245
+ this.sendTrailers = function (streamId, headers) {
1246
+ var s = streams[streamId];
1247
+ if (!s || s.state === STATE.CLOSED) return;
1248
+ var block = encoder.encode(headers);
1249
+ write(Buffer.concat(F.buildHeaderFrames(streamId, block, { endStream: true },
1250
+ remoteSettings.MAX_FRAME_SIZE)));
1251
+ sendEndStream(s);
1252
+ };
1253
+
1254
+ /** Allocate the next stream id for a locally initiated stream. */
1255
+ this.nextStreamId = function () {
1256
+ var id = nextLocalStreamId;
1257
+ nextLocalStreamId += 2;
1258
+ return id;
1259
+ };
1260
+
1261
+ /**
1262
+ * Tell the connection that `bytes` of a stream's body have been dealt with,
1263
+ * returning that much flow-control credit to the peer.
1264
+ *
1265
+ * Only meaningful with autoWindowUpdate: false; with the default the credit
1266
+ * has already been returned and this is a no-op.
1267
+ */
1268
+ this.consume = function (streamId, bytes) {
1269
+ if (autoWindowUpdate) return;
1270
+ var s = streams[streamId];
1271
+ creditNow = true;
1272
+ try {
1273
+ maybeSendConnectionWindowUpdate(0);
1274
+ if (s) {
1275
+ s.pendingCredit = (s.pendingCredit || 0);
1276
+ maybeSendStreamWindowUpdate(s, 0);
1277
+ }
1278
+ } finally { creditNow = false; }
1279
+ };
1280
+
1281
+ /** The `:protocol` value if this stream is an extended CONNECT, else null. */
1282
+ this.getStreamProtocol = function (id) {
1283
+ var s = streams[id];
1284
+ return (s && s.extendedConnect) || null;
1285
+ };
1286
+
1287
+ /** Whether the PEER told us it accepts extended CONNECT. */
1288
+ this.peerAllowsConnectProtocol = function () {
1289
+ return remoteSettings.ENABLE_CONNECT_PROTOCOL === 1;
1290
+ };
1291
+
1292
+ this.getStreamState = function (id) {
1293
+ var s = streams[id];
1294
+ return s ? s.state : STATE.IDLE;
1295
+ };
1296
+
1297
+ this.canSend = function (id) {
1298
+ var s = streams[id];
1299
+ return s ? sendableBytes(s) > 0 : false;
1300
+ };
1301
+
1302
+ /** Whether some stream's DATA waits for the peer's WINDOW_UPDATE. */
1303
+ this.windowBlocked = function () {
1304
+ var blocked = false;
1305
+ waiting.forEach(function (s) {
1306
+ if (s.state !== STATE.CLOSED && s.pendingData.length > 0 && sendableBytes(s) <= 0) blocked = true;
1307
+ });
1308
+ return blocked;
1309
+ };
1310
+
1311
+ Object.defineProperty(this, 'localSettings', { get: function () { return localSettings; } });
1312
+ Object.defineProperty(this, 'remoteSettings', { get: function () { return remoteSettings; } });
1313
+ Object.defineProperty(this, 'activeStreams', { get: function () { return activeCount; } });
1314
+ Object.defineProperty(this, 'goawayReceived', { get: function () { return goawayReceived; } });
1315
+ Object.defineProperty(this, 'closed', { get: function () { return closed; } });
1316
+
1317
+ return this;
1318
+ }
1319
+
1320
+
1321
+ // DATA waits while a socket holds this much unsent (isWriteBlocked) — the
1322
+ // server's and the client's sessions alike.
1323
+ var SOCKET_HIGH_WATER = 256 * 1024;
1324
+
1325
+ export { H2Connection, STATE, SOCKET_HIGH_WATER };