quico 0.3.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.
@@ -0,0 +1,862 @@
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 { EventEmitter } from 'node:events';
16
+ import { Duplex } from 'node:stream';
17
+
18
+ import { H2Connection, SOCKET_HIGH_WATER } from './h2.js';
19
+ import * as F from './h2_frames.js';
20
+ import { buildResponsePseudoHeaders, corkForTick, pseudoHeadersToRequest, runRequestHandler, STATUS_TEXT, maxRequestsPerSocket,
21
+ DEFAULT_HEADERS_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, startDeadline, responsePriority, trailerFields, statusHasNoBody, httpDate, streamResetLimiter } from './http_common.js';
22
+ import { observeH2 } from './h2_fingerprint.js';
23
+ import { IncomingMessage, ServerResponse } from './streams.js';
24
+ import { createTcpListener } from './tcp_listener.js';
25
+
26
+
27
+ // ============================================================
28
+ // HTTP/2 server.
29
+ //
30
+ // The same three layers as everywhere else in this stack: tcp_listener owns
31
+ // the socket and ALPN, h2.js turns bytes into stream events, and streams.js
32
+ // wraps those in the IncomingMessage / ServerResponse pair every framework
33
+ // expects. This file is only the wiring between them — which is why it is a
34
+ // fraction of h2.js's size despite being the thing applications touch.
35
+ //
36
+ // It is also why an Express app runs unchanged over HTTP/2: the req and res
37
+ // it receives are the same objects h1_server and h3_server hand it, and
38
+ // nothing in the middle knows which protocol produced them.
39
+ // ============================================================
40
+
41
+ var DEFAULT_KEEPALIVE_TIMEOUT = 0; // 0 = no idle limit; h2 connections are long-lived
42
+
43
+
44
+ // ============================================================
45
+ // Protocol-independent refusal codes.
46
+ //
47
+ // HTTP/2 and HTTP/3 both let a server refuse a stream, and both use entirely
48
+ // different numbers for it. Code that limits request rates should not have to
49
+ // know which protocol carried the request, so it names an INTENT and each
50
+ // layer maps it to its own registry.
51
+ //
52
+ // REFUSED specifically, and not CANCELLED, is what a rate limiter wants:
53
+ // RFC 9113 §8.1 lets a client safely retry a REFUSED_STREAM on another
54
+ // connection, because it states the request was never processed. Cancelling
55
+ // says the opposite, and a client that retries after it may duplicate work.
56
+ // ============================================================
57
+
58
+ var STREAM_REFUSED = 'refused';
59
+ var STREAM_CANCELLED = 'cancelled';
60
+ var CONN_OVERLOADED = 'overloaded';
61
+
62
+ function translateStreamError(code) {
63
+ if (typeof code === 'number') return code; // caller wants a raw code
64
+ if (code === STREAM_CANCELLED) return F.ERROR.CANCEL;
65
+ return F.ERROR.REFUSED_STREAM;
66
+ }
67
+
68
+ function translateConnError(code) {
69
+ if (typeof code === 'number') return code;
70
+ if (code === CONN_OVERLOADED) return F.ERROR.ENHANCE_YOUR_CALM;
71
+ return F.ERROR.NO_ERROR;
72
+ }
73
+
74
+
75
+ function createServer(options, handler) {
76
+ if (typeof options === 'function') { handler = options; options = {}; }
77
+ options = options || {};
78
+
79
+ var ev = new EventEmitter();
80
+ var listener = null;
81
+ var connections = new Set();
82
+ var closing = false;
83
+
84
+ var altSvc = options.altSvc || null;
85
+ // How long a request may take to arrive (see http_common). headersTimeout
86
+ // bounds a header block left open, which holds the whole connection;
87
+ // requestTimeout each request, from its head to its last byte.
88
+ var headersTimeout = options.headersTimeout != null ? options.headersTimeout : DEFAULT_HEADERS_TIMEOUT;
89
+ var requestTimeout = options.requestTimeout != null ? options.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
90
+ // Rapid Reset (http_common streamResetLimiter): the options checked now.
91
+ streamResetLimiter(options);
92
+ // node:http2's maxSessionMemory: megabytes a connection may have queued.
93
+ var maxSessionMemory = (options.maxSessionMemory > 0 ? options.maxSessionMemory : 10) * 1024 * 1024;
94
+ // node:http's maxRequestsPerSocket, as HTTP/2 can honour it: after the Nth
95
+ // request, GOAWAY — those in flight finish, the client opens a new
96
+ // connection for the next (nginx's keepalive_requests, the same way).
97
+ var maxRequests = maxRequestsPerSocket(options.maxRequestsPerSocket);
98
+
99
+ // node:http's shouldUpgradeCallback, for extended CONNECT (RFC 8441): taken
100
+ // up as an 'upgrade' (true) or handled as an ordinary 'request' (false).
101
+ // Default: true when something listens for 'upgrade'.
102
+ var shouldUpgradeCallback = typeof options.shouldUpgradeCallback === 'function'
103
+ ? options.shouldUpgradeCallback
104
+ : function () { return ev.listenerCount('upgrade') > 0; };
105
+
106
+
107
+ function handleConnection(socket) {
108
+ if (closing) { try { socket.destroy(); } catch (e) {} return; }
109
+
110
+ var h2 = new H2Connection({
111
+ isServer: true,
112
+ stats: options.stats || null,
113
+ maxConcurrentStreams: options.maxConcurrentStreams,
114
+ initialWindowSize: options.initialWindowSize,
115
+ connectionWindowSize: options.connectionWindowSize,
116
+ maxFrameSize: options.maxFrameSize,
117
+ maxHeaderListSize: options.maxHeaderListSize,
118
+ headerTableSize: options.headerTableSize,
119
+ // RFC 8441. On by default: a server that can accept WebSocket over
120
+ // HTTP/2 should say so, or browsers open a second HTTP/1.1 connection
121
+ // instead. Passing false turns it off for a server that has no
122
+ // WebSocket endpoint and no reason to invite extended CONNECT.
123
+ enableConnectProtocol: options.enableConnectProtocol,
124
+ // DATA is held while the socket's unsent bytes are over this mark — the
125
+ // flow-control windows bound what the peer may receive, this what we
126
+ // queue for it (see flushStream in h2.js).
127
+ isWriteBlocked: function () { return socket.writableLength >= SOCKET_HIGH_WATER; },
128
+ // Credit is returned when the application reads, not when the frame
129
+ // arrives. Without this the receive window never closes and a client can
130
+ // push a body far faster than the handler consumes it — the bytes pile
131
+ // up in our heap and flow control does nothing at all.
132
+ autoWindowUpdate: false
133
+ });
134
+
135
+ var streams = {}; // id -> { req, res, deadline }
136
+ var served = 0; // requests on this connection (maxRequestsPerSocket)
137
+ var destroyed = false;
138
+
139
+ // ---- How long a request may take to arrive ----
140
+ //
141
+ // A header block left open (HEADERS without END_HEADERS) holds the
142
+ // connection: no other frame may come until its CONTINUATIONs end it
143
+ // (§6.10). Trickled, it held every other request on the connection with
144
+ // it, and nothing timed it. Past headersTimeout the connection goes —
145
+ // there is no stream to give up instead.
146
+ var headerBlockTimer = null;
147
+ h2.on('header_block_open', function () {
148
+ if (headerBlockTimer) clearTimeout(headerBlockTimer);
149
+ headerBlockTimer = startDeadline(headersTimeout, function () {
150
+ headerBlockTimer = null;
151
+ if (destroyed) return;
152
+ h2.sendGoaway(F.ERROR.NO_ERROR);
153
+ destroy(new Error('timeout: header block not completed within ' + headersTimeout + ' ms'), true);
154
+ });
155
+ });
156
+ h2.on('header_block_closed', function () {
157
+ if (headerBlockTimer) { clearTimeout(headerBlockTimer); headerBlockTimer = null; }
158
+ });
159
+
160
+ // A request whose body has not all arrived within requestTimeout of its
161
+ // head (CONNECT excepted — a tunnel): the stream is reset (CANCEL), both ways, and the request and its
162
+ // response end as they do when the client cancels — 'aborted', and a
163
+ // 'streamError' for the log. Only this stream: the connection serves on.
164
+ // (HTTP/1.1 answers 408 and closes; a stream of its own needs no more
165
+ // than its reset.)
166
+ function armRequestDeadline(streamId, st) {
167
+ st.deadline = startDeadline(requestTimeout, function () {
168
+ st.deadline = null;
169
+ if (streams[streamId] !== st || st.req.complete) return;
170
+ delete streams[streamId];
171
+ h2.sendRstStream(streamId, F.ERROR.CANCEL);
172
+ abortStream(st, 'request not received in full within ' + requestTimeout + ' ms', 'ERR_HTTP_REQUEST_TIMEOUT');
173
+ });
174
+ }
175
+ function clearRequestDeadline(st) {
176
+ if (st && st.deadline) { clearTimeout(st.deadline); st.deadline = null; }
177
+ }
178
+
179
+ // Accumulates the connection's HTTP/2 fingerprint from the opening frames.
180
+ // Attached before execute() is ever called, because everything it needs
181
+ // arrives in the peer's first flight — the fingerprint exists before a
182
+ // single byte of content has been served, which is what makes it useful
183
+ // for deciding whether to serve any.
184
+ var getFingerprint = observeH2(h2);
185
+
186
+ /**
187
+ * What a rate limiter is handed: enough to build a key from, and the two
188
+ * ways to refuse.
189
+ *
190
+ * The methods are named identically to the HTTP/3 equivalents even though
191
+ * the wire mechanisms differ — RST_STREAM on a frame here, RESET_STREAM on
192
+ * a QUIC stream there — so that one piece of limiting code works against
193
+ * both without branching on version.
194
+ */
195
+ var connHandle = {
196
+ httpVersion: '2.0',
197
+ get remoteAddress() { return socket.remoteAddress; },
198
+ get remotePort() { return socket.remotePort; },
199
+ get encrypted() { return !!socket.encrypted; },
200
+ // The session ticket: { data, resumed, offered } (see lemon-tls
201
+ // getTicketInfo); null over cleartext.
202
+ getTicketInfo: function () { return socket.getTicketInfo ? socket.getTicketInfo() : null; },
203
+ isSessionReused: function () { return !!(socket.isSessionReused && socket.isSessionReused()); },
204
+ get activeStreams() { return Object.keys(streams).length; },
205
+ fingerprint: getFingerprint,
206
+
207
+ sendRstStream: function (streamId, code) {
208
+ h2.sendRstStream(streamId, translateStreamError(code));
209
+ },
210
+
211
+ // Stops new streams; in-flight ones finish. See the note in h2.js.
212
+ goaway: function (code) { h2.goaway(translateConnError(code)); },
213
+
214
+ // For when stopping is not enough.
215
+ destroy: function () { destroy(null); }
216
+ };
217
+
218
+ h2.on('stream_open', function (streamId) {
219
+ ev.emit('stream_open', connHandle, streamId);
220
+ });
221
+
222
+ // Rapid Reset: past streamResetBurst / streamResetRate, GOAWAY
223
+ // ENHANCE_YOUR_CALM and the connection closed, as node:http2 does.
224
+ var resetTooMany = streamResetLimiter(options);
225
+ h2.on('stream_reset', function (streamId, code) {
226
+ ev.emit('stream_reset', connHandle, streamId, code);
227
+ if (destroyed || !resetTooMany()) return;
228
+ var rr = new Error('too many stream resets (streamResetBurst, streamResetRate)');
229
+ rr.code = 'ERR_HTTP2_STREAM_RESET_RATE';
230
+ try { h2.goaway(F.ERROR.ENHANCE_YOUR_CALM); } catch (e) {}
231
+ ev.emit('sessionError', rr, connHandle);
232
+ destroy(rr, true);
233
+ });
234
+
235
+ // The response is complete — END_STREAM written — and the request is
236
+ // not (RFC 9113 §8.1): a 413, a 401, a refusal answered before the upload
237
+ // is read. Unless the application is reading the body, the client is
238
+ // told to stop sending it: RST_STREAM with NO_ERROR, after the response,
239
+ // which a client must not take as a failure of it. node:http2 decides it
240
+ // the same way (the stream is closed if nothing has read from it). The
241
+ // stream used to stay open: the client went on uploading into a window
242
+ // nobody would open again, and held one of its concurrent streams until
243
+ // it gave up.
244
+ h2.on('local_end', function (streamId) {
245
+ var st = streams[streamId]; // taken now: the response's end removes the entry
246
+ if (!st || !st.req || st.duplex) return;
247
+ process.nextTick(function () {
248
+ var req = st.req;
249
+ if (req.complete || req.aborted || destroyed) return;
250
+ if (!req.destroyed && req.readableFlowing !== null) return; // still being read
251
+ if (streams[streamId] === st) delete streams[streamId];
252
+ clearRequestDeadline(st);
253
+ h2.sendRstStream(streamId, F.ERROR.NO_ERROR);
254
+ });
255
+ });
256
+
257
+ connections.add(socket);
258
+
259
+ // `linger`: end the socket rather than destroy it — what was written last
260
+ // (the GOAWAY saying why) leaves first — and destroy it if the peer has
261
+ // not closed its side in time. destroy() right after end() dropped the
262
+ // GOAWAY the comment below meant to deliver.
263
+ function destroy(err, linger) {
264
+ if (destroyed) return;
265
+ destroyed = true;
266
+ connections.delete(socket);
267
+ if (headerBlockTimer) { clearTimeout(headerBlockTimer); headerBlockTimer = null; }
268
+ for (var id in streams) {
269
+ var st = streams[id];
270
+ clearRequestDeadline(st);
271
+ // An extended-CONNECT tunnel (a WebSocket, CONNECT-UDP) goes with the
272
+ // connection: its Duplex is destroyed. Its request had ended at the
273
+ // accept, so the line below never reached it — the tunnel's user
274
+ // waited for an end that could not come.
275
+ if (st.duplex) { try { st.duplex.destroy(); } catch (e) {} continue; }
276
+ // 'aborted', an Error only to a listener, 'close' (see _abort); the
277
+ // response, mid-write, closed (_abortResponse).
278
+ if (st.req && !st.req.complete) st.req._abort(err || null);
279
+ if (st.res) st.res._abortResponse();
280
+ }
281
+ streams = {};
282
+ if (!linger) { try { socket.destroy(); } catch (e) {} return; }
283
+ try { socket.end(); } catch (e) {}
284
+ var t = setTimeout(function () { try { socket.destroy(); } catch (e) {} }, 1000);
285
+ if (t.unref) t.unref();
286
+ }
287
+
288
+ // An HTTP/2 connection's errors are 'sessionError' (err, session), as
289
+ // node:http2 reports them — not 'clientError' (err, socket), which is
290
+ // HTTP/1.1's, and whose listeners answer by writing an HTTP/1.1 status
291
+ // line onto the socket: on an HTTP/2 connection, after its GOAWAY, that is
292
+ // bytes the client cannot parse.
293
+ h2.on('send', function (data) {
294
+ if (destroyed || !socket.writable) return;
295
+ corkForTick(socket);
296
+ try { socket.write(data); } catch (e) { destroy(e); return; }
297
+ // What is queued on the socket and not yet sent, bounded per connection
298
+ // (node's maxSessionMemory, in MB, default 10). DATA waits for the
299
+ // socket (isWriteBlocked), so what can pile up here is everything else —
300
+ // replies the peer provokes (PING and SETTINGS acknowledgements, resets)
301
+ // while it does not read. Past the bound the connection goes.
302
+ if (socket.writableLength > maxSessionMemory) {
303
+ var e = new Error('peer is not reading: ' + socket.writableLength + ' bytes queued');
304
+ e.code = 'ERR_HTTP2_SESSION_MEMORY';
305
+ ev.emit('sessionError', e, connHandle);
306
+ destroy(e);
307
+ }
308
+ });
309
+
310
+ // DATA was waiting for the socket; it has room again.
311
+ socket.on('drain', function () { if (!destroyed) h2.flushAll(); });
312
+
313
+ h2.on('error', function (err) {
314
+ ev.emit('sessionError', err, connHandle);
315
+ // GOAWAY has already been queued by the connection layer; let it reach
316
+ // the peer before the socket goes, so the client learns why.
317
+ destroy(err, true);
318
+ });
319
+
320
+ socket.on('data', function (chunk) {
321
+ if (destroyed) return;
322
+ try { h2.execute(chunk); }
323
+ catch (err) { ev.emit('sessionError', err, connHandle); destroy(err); }
324
+ });
325
+
326
+ socket.on('error', function (err) {
327
+ ev.emit('sessionError', err, connHandle);
328
+ destroy(err);
329
+ });
330
+
331
+ socket.on('close', function () { destroy(null); });
332
+
333
+ // The client half-closes (TCP FIN, or TLS close_notify): it will send
334
+ // nothing more, and still reads (RFC 9293 §3.6; RFC 9113 has nothing
335
+ // against it). The connection was destroyed on the spot, and every
336
+ // response in progress with it. Now: a request whose body was still
337
+ // arriving cannot complete — reset (CANCEL), the request 'aborted';
338
+ // the ones fully received are answered; and when the last stream closes,
339
+ // GOAWAY and our own end. The socket is half-open for this connection
340
+ // only (HTTP/1.1 ends its socket with the client's, as node:http does).
341
+ socket.allowHalfOpen = true;
342
+ var peerEnded = false;
343
+ function finishIfDone() {
344
+ if (destroyed || !peerEnded || h2.activeStreams > 0) return;
345
+ try { h2.sendGoaway(F.ERROR.NO_ERROR); } catch (e) {}
346
+ destroy(null, true);
347
+ }
348
+ socket.on('end', function () {
349
+ if (destroyed) return;
350
+ peerEnded = true;
351
+ if (h2.windowBlocked()) { destroy(null); return; } // see 'window_blocked' below
352
+ for (var id in streams) {
353
+ var st = streams[id];
354
+ // A tunnel (WebSocket, CONNECT-UDP) cannot go on: nothing more can
355
+ // come from the client on it. Reset and destroyed — left open, it
356
+ // held the connection half-open for good (finishIfDone waits for it).
357
+ if (st.duplex) {
358
+ delete streams[id];
359
+ h2.sendRstStream(Number(id), F.ERROR.CANCEL);
360
+ try { st.duplex.destroy(); } catch (e) {}
361
+ continue;
362
+ }
363
+ if (st.req && !st.req.complete && !st.req.aborted) {
364
+ delete streams[id];
365
+ clearRequestDeadline(st);
366
+ h2.sendRstStream(Number(id), F.ERROR.CANCEL);
367
+ st.req._abort(null);
368
+ if (st.res) st.res._abortResponse();
369
+ }
370
+ }
371
+ finishIfDone();
372
+ });
373
+ h2.on('stream_close', function (id) {
374
+ // A tunnel closed both ways: its entry goes (nothing else removes it —
375
+ // a long-lived connection kept every WebSocket it ever carried).
376
+ var st = streams[id];
377
+ if (st && st.duplex) delete streams[id];
378
+ if (peerEnded) process.nextTick(finishIfDone);
379
+ });
380
+ // A response that waits for the client to open a window, from a client
381
+ // that has half-closed: it sends nothing more — no WINDOW_UPDATE either —
382
+ // so the response can never finish, and the connection would be held
383
+ // open for good (a client that destroyed its socket looks the same until
384
+ // a write reaches it, and none would). Over, as before half-close.
385
+ h2.on('window_blocked', function () {
386
+ if (!peerEnded || destroyed) return;
387
+ process.nextTick(function () { if (!destroyed) destroy(null); });
388
+ });
389
+
390
+
391
+ // ---- Requests ----
392
+
393
+ h2.on('headers', function (streamId, headers, endStream, connectProtocol) {
394
+
395
+ // ---- Extended CONNECT (RFC 8441) ----
396
+ //
397
+ // A CONNECT carrying `:protocol` is not a request/response exchange but
398
+ // a bidirectional channel — this is how WebSocket runs over HTTP/2. The
399
+ // application gets it through the SAME 'upgrade' event that HTTP/1.1
400
+ // uses, so a WebSocketServer attached to this server works over both
401
+ // without knowing which carried the connection.
402
+ //
403
+ // What it gets is a Duplex over the stream rather than a socket: reads
404
+ // are DATA frames, writes are DATA frames, and the HTTP/2 connection
405
+ // underneath keeps serving every other stream. That is the whole
406
+ // advantage over HTTP/1.1, where an upgrade consumes the connection.
407
+ if (connectProtocol) {
408
+ var parsedC = pseudoHeadersToRequest(headers);
409
+ var creq = new IncomingMessage({
410
+ httpVersion: '2.0',
411
+ method: 'CONNECT',
412
+ url: parsedC.url,
413
+ headers: parsedC.headers,
414
+ streamId: streamId,
415
+ socket: socket
416
+ });
417
+ creq.rawHeaders = [];
418
+ creq.headers[':protocol'] = connectProtocol;
419
+ }
420
+ // Not taken up: an ordinary request, answered by the handler — as
421
+ // node:http does with an upgrade nobody asked for.
422
+ if (connectProtocol && !shouldUpgradeCallback(creq)) connectProtocol = null;
423
+ if (connectProtocol) {
424
+ creq._endData();
425
+
426
+ var stream = makeStreamDuplex(streamId);
427
+ streams[streamId] = { req: creq, res: null, duplex: stream };
428
+
429
+ if (ev.listenerCount('upgrade') === 0) {
430
+ // Accepted by shouldUpgradeCallback, yet nobody listens. Where
431
+ // node:http destroys the socket, here only this stream is lost: §4
432
+ // says to answer with an error status rather than leave the client
433
+ // waiting for a 200 that will never come.
434
+ h2.sendHeaders(streamId, [[':status', '501']], true);
435
+ delete streams[streamId];
436
+ return;
437
+ }
438
+ ev.emit('upgrade', creq, stream, Buffer.alloc(0));
439
+ return;
440
+ }
441
+
442
+ var parsed = pseudoHeadersToRequest(headers);
443
+
444
+ var req = new IncomingMessage({
445
+ httpVersion: '2.0',
446
+ method: parsed.method,
447
+ url: parsed.url,
448
+ headers: parsed.headers,
449
+ streamId: streamId,
450
+ socket: socket
451
+ });
452
+ req.scheme = headers[':scheme'];
453
+ req.authority = headers[':authority'];
454
+ // Its HEADERS read from 0-RTT data (TLS early data): see h1_server.
455
+ req.earlyData = socket.isEarlyData === true;
456
+ // Per-connection, not per-request: the same function on every request of
457
+ // a connection returns the same fingerprint, because it describes how
458
+ // the connection was set up.
459
+ req.h2Fingerprint = getFingerprint;
460
+
461
+ var res = new ServerResponse({ streamId: streamId, socket: socket });
462
+ res.req = req;
463
+ streams[streamId] = { req: req, res: res };
464
+
465
+ // Back-pressure: a Readable that nobody is reading must slow the peer
466
+ // down, and the only lever h2 has is the flow-control window. Credit
467
+ // goes back as the application READS: from _read, which the stream
468
+ // calls when its consumer wants more. Bytes that arrived are counted in
469
+ // (see 'data' below) and returned there.
470
+ //
471
+ // This was a 'data' listener, which says the same in its comment and
472
+ // the opposite in effect: a 'data' listener switches a Readable to
473
+ // flowing, so every body chunk was emitted the moment it arrived — with
474
+ // nobody listening yet if the handler did anything asynchronous before
475
+ // reading the body (an auth check, a lookup). The chunks and the 'end'
476
+ // were lost, and the request waited forever for a body it had already
477
+ // been given. node:http keeps a request paused until someone reads it;
478
+ // so does this now.
479
+ var uncredited = 0;
480
+ req._h2Arrived = function (n) { uncredited += n; };
481
+ req._read = function () {
482
+ if (uncredited > 0) { h2.consume(streamId, uncredited); uncredited = 0; }
483
+ };
484
+
485
+ wireResponse(streamId, req, res);
486
+
487
+ if (endStream) req._endData();
488
+ else if (req.method !== 'CONNECT') armRequestDeadline(streamId, streams[streamId]); // a tunnel stays open
489
+
490
+ // A handler that throws loses its own stream, reset INTERNAL_ERROR —
491
+ // not the connection, and not the other requests on it (see
492
+ // runRequestHandler). A response it had already ended is left to finish.
493
+ if (maxRequests > 0 && ++served === maxRequests) h2.goaway(F.ERROR.NO_ERROR);
494
+ runRequestHandler(function () {
495
+ if (typeof handler === 'function') handler(req, res);
496
+ ev.emit('request', req, res);
497
+ }, function () {
498
+ if (res.writableEnded || streams[streamId] === undefined) return;
499
+ clearRequestDeadline(streams[streamId]);
500
+ delete streams[streamId];
501
+ h2.sendRstStream(streamId, F.ERROR.INTERNAL_ERROR);
502
+ req._abort(null);
503
+ res._abortResponse();
504
+ });
505
+ });
506
+
507
+ h2.on('data', function (streamId, chunk) {
508
+ var st = streams[streamId];
509
+ if (!st) return;
510
+ if (st.duplex) { st.duplex.push(chunk); h2.consume(streamId, chunk.length); return; }
511
+ if (st.req) {
512
+ // Counted before the push: a consumer already reading pulls
513
+ // synchronously from inside it, and _read hands the credit back.
514
+ st.req._h2Arrived(chunk.length);
515
+ st.req._pushData(chunk);
516
+ }
517
+ });
518
+
519
+ h2.on('trailers', function (streamId, trailers) {
520
+ var st = streams[streamId];
521
+ if (st && st.req) st.req.trailers = trailers;
522
+ });
523
+
524
+ h2.on('end', function (streamId) {
525
+ var st = streams[streamId];
526
+ if (!st) return;
527
+ if (st.duplex) { st.duplex.push(null); return; }
528
+ clearRequestDeadline(st);
529
+ if (st.req) st.req._endData();
530
+ if (st.responseDone) delete streams[streamId];
531
+ });
532
+
533
+ h2.on('rst', function (streamId, code) {
534
+ var st = streams[streamId];
535
+ if (!st) return;
536
+ delete streams[streamId];
537
+ clearRequestDeadline(st);
538
+ if (st.duplex) { st.duplex.destroy(); return; }
539
+ abortStream(st, 'stream reset: ' + (F.ERROR_NAME[code] || code),
540
+ 'ERR_HTTP2_STREAM_ERROR');
541
+ });
542
+
543
+ /**
544
+ * End a request that the network killed under it.
545
+ *
546
+ * A reset arrives because the PEER cancelled, which is ordinary traffic —
547
+ * a browser navigating away resets every in-flight stream. Destroying the
548
+ * IncomingMessage with an Error means an application that never registered
549
+ * an 'error' handler takes the whole process down with it, and almost none
550
+ * do: node:http teaches people to listen for 'aborted', not 'error'.
551
+ *
552
+ * So: 'aborted' always, an Error only to a listener that asked for one, and
553
+ * the server's own 'streamError' for logging either way. h2spec found this
554
+ * on its fourth test — it resets a stream as a matter of course, and the
555
+ * server died.
556
+ */
557
+ function abortStream(st, message, code) {
558
+ var err = new Error(message);
559
+ err.code = code;
560
+
561
+ if (st.req) st.req._abort(err); // 'aborted', an Error only to a listener, 'close'
562
+ // The response gets 'close' only, as node:http's and node:http2's do
563
+ // when the client goes. An 'error' here was guarded by "a listener
564
+ // asked for it" — but pipe() is such a listener, and rethrows when it
565
+ // is the only one: a client cancelling a piped response took the
566
+ // server down.
567
+ if (st.res) st.res._abortResponse();
568
+ ev.emit('streamError', err, st.req, st.res);
569
+ }
570
+
571
+ h2.on('goaway', function () {
572
+ // The client will not open new streams; existing ones run to completion
573
+ // and the socket closes when they do.
574
+ closingConnection = true;
575
+ });
576
+
577
+ var closingConnection = false;
578
+
579
+
580
+ /**
581
+ * A Duplex over one HTTP/2 stream, for extended CONNECT.
582
+ *
583
+ * ws_server was written against a socket: it calls write(), listens for
584
+ * 'data', and destroys when finished. Giving it this instead of a socket
585
+ * is the entire integration — the WebSocket framing code never learns
586
+ * whether its bytes are TCP or DATA frames.
587
+ *
588
+ * The `_h2Accept` hook is the one thing a socket does not have: an HTTP/2
589
+ * extended CONNECT is accepted with a 200 response rather than a 101, so
590
+ * the layer above needs a way to say "accepted" that is not writing bytes.
591
+ */
592
+ function makeStreamDuplex(streamId) {
593
+ var stream = new Duplex({
594
+ read: function () {},
595
+ write: function (chunk, encoding, callback) {
596
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
597
+ var more = h2.sendData(streamId, chunk, false);
598
+ if (more) { callback(); return; }
599
+ // Send window closed: hold the callback so back-pressure reaches
600
+ // whoever is writing, exactly as a socket would.
601
+ var released = false;
602
+ function onDrain(id) {
603
+ if (id !== streamId || released) return;
604
+ released = true;
605
+ h2.off('drain', onDrain);
606
+ callback();
607
+ }
608
+ h2.on('drain', onDrain);
609
+ },
610
+ final: function (callback) {
611
+ h2.sendData(streamId, null, true);
612
+ callback();
613
+ },
614
+ // Destroyed by its user while the stream is open: RST_STREAM CANCEL,
615
+ // so the peer hears the tunnel is gone. (Destroyed by a reset or the
616
+ // connection's end, the entry is already gone and nothing is sent.)
617
+ destroy: function (err, callback) {
618
+ var st = streams[streamId];
619
+ if (st && st.duplex === stream && !destroyed) {
620
+ delete streams[streamId];
621
+ try { h2.sendRstStream(streamId, F.ERROR.CANCEL); } catch (e) {}
622
+ }
623
+ callback(err);
624
+ }
625
+ });
626
+
627
+ stream._streamId = streamId;
628
+ stream.encrypted = !!socket.encrypted;
629
+ stream.remoteAddress = socket.remoteAddress;
630
+ stream.remotePort = socket.remotePort;
631
+ stream.setNoDelay = function () { return stream; };
632
+ stream.setKeepAlive = function () { return stream; };
633
+ stream.setTimeout = function () { return stream; };
634
+
635
+ /**
636
+ * Accept the extended CONNECT.
637
+ *
638
+ * HTTP/2 has no 101: RFC 8441 §4 says a 2xx response accepts the channel
639
+ * and anything else refuses it. ws_server calls this instead of writing
640
+ * an HTTP/1.1 status line — which is why the WebSocket handshake code
641
+ * has to know which transport it is on for exactly one line, and no more.
642
+ */
643
+ stream._h2Accept = function (extraHeaders) {
644
+ var list = [[':status', '200']];
645
+ for (var n in (extraHeaders || {})) list.push([String(n).toLowerCase(), extraHeaders[n]]);
646
+ h2.sendHeaders(streamId, list, false);
647
+ };
648
+
649
+ /** Refuse it, with a status (and fields: a Proxy-Status, say). */
650
+ stream._h2Reject = function (status, message, extraHeaders) {
651
+ var list = [[':status', String(status)]];
652
+ for (var n in (extraHeaders || {})) list.push([String(n).toLowerCase(), String(extraHeaders[n])]);
653
+ h2.sendHeaders(streamId, list, !message);
654
+ if (message) h2.sendData(streamId, Buffer.from(String(message)), true);
655
+ delete streams[streamId];
656
+ };
657
+
658
+ return stream;
659
+ }
660
+
661
+
662
+ // ============================================================
663
+ // Response wiring
664
+ //
665
+ // ServerResponse is transport-agnostic — h1_server overrides these same
666
+ // three hooks to write HTTP/1.1 bytes, h3_server to write H3 frames.
667
+ // ============================================================
668
+
669
+ function wireResponse(streamId, req, res) {
670
+ var bodyAllowed = true;
671
+ var finished = false;
672
+
673
+ res._flushHeaders = function () {
674
+ if (res.headersSent || res._aborted || destroyed) return;
675
+ var headers = res.getHeaders();
676
+ var code = res.statusCode;
677
+
678
+ // HTTP/2 has no reason phrase and no connection-specific fields.
679
+ // buildResponsePseudoHeaders drops the latter; sending them would make
680
+ // the message malformed for the peer and, worse, would be forwarded by
681
+ // a proxy into an HTTP/1.1 hop where they change the framing.
682
+ delete headers['connection'];
683
+ delete headers['keep-alive'];
684
+ delete headers['transfer-encoding'];
685
+ delete headers['upgrade'];
686
+
687
+ bodyAllowed = !statusHasNoBody(code) && req.method !== 'HEAD';
688
+
689
+ if (!('date' in headers)) headers['date'] = httpDate();
690
+ // A function when the server was built before its port was known —
691
+ // the unified server builds its layers at creation and learns the
692
+ // port from listen(), or from options.port when fed from outside.
693
+ var altSvcNow = typeof altSvc === 'function' ? altSvc() : altSvc;
694
+ if (altSvcNow && !('alt-svc' in headers)) headers['alt-svc'] = altSvcNow;
695
+
696
+ var list = buildResponsePseudoHeaders(code, headers);
697
+ try { h2.sendHeaders(streamId, list, false); }
698
+ catch (e) { destroy(e); return; }
699
+ res.headersSent = true;
700
+ };
701
+
702
+ res._write = function (chunk, encoding, callback) {
703
+ // The peer reset this stream while the handler was still producing.
704
+ // Discard rather than error: the handler is mid-flight and cannot be
705
+ // expected to check on every write.
706
+ if (res._aborted || destroyed) { callback(); return; }
707
+ if (!res.headersSent) res._flushHeaders();
708
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
709
+
710
+ // A HEAD response, or a bodiless status: the application still wrote
711
+ // the body it WOULD have sent, and the headers already describe it.
712
+ if (!bodyAllowed) { callback(); return; }
713
+
714
+ var more = h2.sendData(streamId, chunk, false);
715
+ if (more) { callback(); return; }
716
+
717
+ // The send window is closed. Hold the callback until it reopens —
718
+ // that is what makes res.write() return false and a piped Readable
719
+ // pause, instead of queueing the whole body in memory.
720
+ var released = false;
721
+ function onDrain(id) {
722
+ if (id !== streamId || released) return;
723
+ released = true;
724
+ h2.off('drain', onDrain);
725
+ callback();
726
+ }
727
+ h2.on('drain', onDrain);
728
+ };
729
+
730
+ res._final = function (callback) {
731
+ if (res._aborted || destroyed) { callback(); return; }
732
+ if (finished) { callback(); return; }
733
+ finished = true;
734
+ if (!res.headersSent) res._flushHeaders();
735
+ var trailers = trailerFields(res.trailers);
736
+ if (trailers) {
737
+ h2.sendTrailers(streamId, trailers);
738
+ } else {
739
+ h2.sendData(streamId, null, true);
740
+ }
741
+ // The entry goes when the request is over too. Removed here, with the
742
+ // request body still arriving — an application reading it after
743
+ // answering — every DATA frame after the response was dropped, its
744
+ // flow-control credit never returned, and the client's upload stalled.
745
+ var st = streams[streamId];
746
+ if (st) {
747
+ st.responseDone = true;
748
+ if (req.complete || req.aborted) delete streams[streamId];
749
+ }
750
+ callback();
751
+ };
752
+
753
+ /**
754
+ * 103 Early Hints (RFC 8297): a HEADERS frame with :status 103, the
755
+ * stream left open — the final response follows on it. node:http2's
756
+ * signature: (hints, callback). Nothing once the head has gone.
757
+ */
758
+ res.writeEarlyHints = function (hints, callback) {
759
+ if (!res.headersSent && !res._aborted && !destroyed) {
760
+ var h = {};
761
+ for (var n in (hints || {})) {
762
+ if (Object.prototype.hasOwnProperty.call(hints, n) && hints[n] != null) h[String(n).toLowerCase()] = hints[n];
763
+ }
764
+ try { h2.sendHeaders(streamId, buildResponsePseudoHeaders(103, h), false); }
765
+ catch (e) { /* the stream went; the final response will find out */ }
766
+ }
767
+ if (typeof callback === 'function') process.nextTick(callback);
768
+ };
769
+
770
+ /**
771
+ * The response's priority, as the server decides it: { urgency (0..7,
772
+ * 0 first), incremental } — RFC 9218, as h3_server's. It replaces what
773
+ * the client asked for, and later requests from it do not override it.
774
+ */
775
+ res.setPriority = function (p) { h2.setStreamPriority(streamId, responsePriority(p)); };
776
+
777
+ /** Reset the stream — the HTTP/2 way to abandon a response. */
778
+ res.destroyStream = function (code) {
779
+ h2.sendRstStream(streamId, code === undefined ? F.ERROR.CANCEL : code);
780
+ clearRequestDeadline(streams[streamId]);
781
+ delete streams[streamId];
782
+ };
783
+
784
+ // The application giving the exchange up — res.destroy([err]),
785
+ // req.destroy([err]), either of which ends the stream on node:http2:
786
+ // the response unfinished, RST_STREAM CANCEL (INTERNAL_ERROR for a
787
+ // failure of ours); complete, RST_STREAM NO_ERROR for the rest of the
788
+ // request body (RFC 9113 §8.1). It was local only: the client waited
789
+ // for a response that would not come, the stream open until the
790
+ // connection closed.
791
+ var giveUp = function (err) {
792
+ if (destroyed || !streams[streamId]) return;
793
+ if (!finished) res.destroyStream(err ? F.ERROR.INTERNAL_ERROR : F.ERROR.CANCEL);
794
+ else if (!req.complete) res.destroyStream(F.ERROR.NO_ERROR);
795
+ };
796
+ res._onDestroy(giveUp);
797
+ req._onDestroy(function (err) { if (!res.writableFinished && !res.destroyed) res.destroy(err); else giveUp(err); });
798
+ }
799
+
800
+ // The client speaks first, but the server's SETTINGS must go out
801
+ // immediately — a client may not send anything until it has ours.
802
+ h2.sendPreface();
803
+ }
804
+
805
+
806
+ // ============================================================
807
+ // Listening
808
+ //
809
+ // Convenience only, for an h2-only server. The unified server builds one
810
+ // tcp_listener and registers this handleConnection under 'h2' alongside
811
+ // h1_server's under 'http/1.1'.
812
+ // ============================================================
813
+
814
+ function listen(port, host, callback) {
815
+ if (typeof host === 'function') { callback = host; host = undefined; }
816
+
817
+ listener = createTcpListener({
818
+ tls: options.tls,
819
+ key: options.key,
820
+ cert: options.cert,
821
+ ca: options.ca,
822
+ SNICallback: options.SNICallback
823
+ });
824
+ listener.register('h2', handleConnection);
825
+ listener.on('error', function (err) { ev.emit('error', err); });
826
+ listener.on('clientError', function (err, sock) { ev.emit('clientError', err, sock); });
827
+ listener.on('tlsClientError', function (err, sock) { ev.emit('tlsClientError', err, sock); });
828
+ listener.listen(port, host, function () {
829
+ ev.emit('listening');
830
+ if (typeof callback === 'function') callback();
831
+ });
832
+ return server;
833
+ }
834
+
835
+ function close(callback) {
836
+ closing = true;
837
+ if (!listener) {
838
+ for (var s of connections) { try { s.end(); } catch (e) {} }
839
+ if (callback) callback();
840
+ return;
841
+ }
842
+ listener.close(callback);
843
+ }
844
+
845
+
846
+ var server = {
847
+ listen: listen,
848
+ close: close,
849
+ address: function () { return listener && listener.address ? listener.address() : null; },
850
+ handleConnection: handleConnection,
851
+ on: function (n, f) { ev.on(n, f); return server; },
852
+ once: function (n, f) { ev.once(n, f); return server; },
853
+ off: function (n, f) { ev.off(n, f); return server; },
854
+ emit: function () { ev.emit.apply(ev, arguments); return server; },
855
+ listenerCount: function (n) { return ev.listenerCount(n); }
856
+ };
857
+
858
+ return server;
859
+ }
860
+
861
+
862
+ export { createServer, STREAM_REFUSED, STREAM_CANCELLED, CONN_OVERLOADED };