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.
@@ -0,0 +1,93 @@
1
+ // ============================================================
2
+ // The server's counters — server.getStats()
3
+ // ============================================================
4
+ //
5
+ // What an operator watches a server by: how many connections came, over what,
6
+ // how many handshakes failed, how connections ended and with what code, what
7
+ // 0-RTT did, how often the anti-amplification limit held the server back,
8
+ // how many internal errors were caught. Counts only: no addresses, no names,
9
+ // no headers, no payloads — nothing here identifies a client.
10
+ //
11
+ // One object per server, made by createServer and handed to each layer
12
+ // (quic_server, quic_connection, h3_server, tcp_listener, h2_server / h2.js,
13
+ // h1_server) in its options as `stats`; each counts where the thing happens,
14
+ // with a plain increment. A layer used on its own (createQuicServer, an
15
+ // H2Connection) has none and counts nothing.
16
+ //
17
+ // Codes are the protocol's, as hex strings ('0x0', '0x12f'): a map from code
18
+ // to count, only for codes seen.
19
+
20
+ function ServerStats() {
21
+ this.since = Date.now();
22
+ // Connections taken: QUIC, a first Initial that began a handshake; TCP, a
23
+ // socket accepted (before TLS).
24
+ this.connections = { quic: 0, tcp: 0 };
25
+ // TLS handshakes, as they ended: completed (the client's Finished
26
+ // verified) or failed (an alert either way, a timeout, the connection
27
+ // closed first). resumed: completed with a session ticket.
28
+ this.handshakes = { quic: { completed: 0, failed: 0 }, tcp: { completed: 0, failed: 0 }, resumed: 0 };
29
+ // Connections by the protocol they went on to speak (the ALPN agreed; over
30
+ // cleartext TCP, the preface read) — other: an ALPN name of the
31
+ // application's.
32
+ this.protocols = { h3: 0, h2: 0, 'http/1.1': 0, other: 0 };
33
+ // 0-RTT offered by the client: taken, or refused (the ticket's, the replay
34
+ // check, a HelloRetryRequest); tooEarly, requests answered 425 (RFC 8470).
35
+ this.earlyData = { accepted: 0, rejected: 0, tooEarly: 0 };
36
+ // How connections ended, by code:
37
+ // quic CONNECTION_CLOSE of the transport (RFC 9000 §20.1; 0x100 + a TLS
38
+ // alert, RFC 9001 §4.8) — sent by us, received from the client;
39
+ // silent: ended with none either way (the idle timeout, a
40
+ // stateless reset, a handshake abandoned)
41
+ // h3 CONNECTION_CLOSE of the application (RFC 9114 §8.1)
42
+ // h2 GOAWAY (RFC 9113 §7)
43
+ // http1 requests refused before the application saw them, by status
44
+ // (400 a malformed request, 431 headers too large, ...)
45
+ this.closes = {
46
+ quic: { sent: {}, received: {}, silent: 0 },
47
+ h3: { sent: {}, received: {} },
48
+ h2: { sent: {}, received: {} },
49
+ http1: { sent: {} }
50
+ };
51
+ // Streams reset, by code: RESET_STREAM (HTTP/3) and RST_STREAM (HTTP/2),
52
+ // sent and received.
53
+ this.streamResets = { h3: { sent: {}, received: {} }, h2: { sent: {}, received: {} } };
54
+ // Datagrams the server held because the client's address was not yet
55
+ // validated and it had sent three times what it received (RFC 9000 §8.1).
56
+ this.amplificationBlocked = 0;
57
+ // Exceptions caught inside the QUIC and HTTP/3 layers: a bug of ours,
58
+ // never the client's (the packet dropped, or H3_INTERNAL_ERROR).
59
+ this.internalErrors = 0;
60
+ }
61
+
62
+ /** One more of a protocol error `code` in `map`, by its hex. */
63
+ ServerStats.prototype.code = function (map, code) {
64
+ // Up to 62 bits (a QUIC varint — WebTransport's codes are past 2^32): by
65
+ // its value, not cut to 32 bits.
66
+ var k = '0x' + (typeof code === 'bigint' ? code : Number(code)).toString(16);
67
+ map[k] = (map[k] || 0) + 1;
68
+ };
69
+
70
+ /** One more of an HTTP status in `map`. */
71
+ ServerStats.prototype.status = function (map, status) {
72
+ map[status] = (map[status] || 0) + 1;
73
+ };
74
+
75
+ /** A TLS handshake ended ('quic' or 'tcp'): completed or not, resumed. */
76
+ ServerStats.prototype.handshake = function (transport, ok, resumed) {
77
+ this.handshakes[transport][ok ? 'completed' : 'failed']++;
78
+ if (ok && resumed) this.handshakes.resumed++;
79
+ };
80
+
81
+ /** A connection handed to the protocol it speaks. */
82
+ ServerStats.prototype.protocol = function (name) {
83
+ this.protocols[name === 'h3' || name === 'h2' || name === 'http/1.1' ? name : 'other']++;
84
+ };
85
+
86
+ /** A copy, for the caller to keep or change. */
87
+ ServerStats.prototype.snapshot = function () {
88
+ var copy = JSON.parse(JSON.stringify(this));
89
+ copy.uptime = Date.now() - this.since;
90
+ return copy;
91
+ };
92
+
93
+ export { ServerStats };
package/src/streams.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import { Readable, Writable, Duplex } from 'node:stream';
16
+ import { validateHeaderName, validateHeaderValue, validateStatus, validateRequestTarget, headersSentError, responsePriority } from './http_common.js';
16
17
 
17
18
 
18
19
  // ============================================================
@@ -52,6 +53,8 @@ class IncomingMessage extends Readable {
52
53
  // Server-side (request)
53
54
  this.method = options.method || null;
54
55
  this.url = options.url || null;
56
+ // HTTP/3: the request came in 0-RTT (it may be a replay). Never over TCP.
57
+ this.earlyData = false;
55
58
 
56
59
  // QUIC has no TCP socket — stub so libraries don't crash
57
60
  this.socket = options.socket || _makeSocketStub();
@@ -73,13 +76,15 @@ class IncomingMessage extends Readable {
73
76
  /**
74
77
  * Called by H3 layer when a body chunk arrives.
75
78
  */
79
+ // push()'s answer: false once the buffer is full — the caller stops
80
+ // reading until _read asks for more.
76
81
  _pushData(chunk) {
77
- if (this.complete) return;
82
+ if (this.complete) return true;
78
83
  if (typeof chunk === 'string') chunk = Buffer.from(chunk);
79
84
  if (chunk instanceof Uint8Array && !Buffer.isBuffer(chunk)) {
80
85
  chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
81
86
  }
82
- this.push(chunk);
87
+ return this.push(chunk);
83
88
  }
84
89
 
85
90
  /**
@@ -92,14 +97,31 @@ class IncomingMessage extends Readable {
92
97
  }
93
98
 
94
99
  /**
95
- * Abort the stream (error / timeout / reset).
100
+ * The exchange ended before its body did — the peer reset the stream or
101
+ * went away, or (client side) req.destroy(). The one place this is decided,
102
+ * as node:http's IncomingMessage does it:
103
+ *
104
+ * 'aborted' — always, once;
105
+ * 'error' — ('aborted', ECONNRESET, or the cause given) only to a
106
+ * listener that asked for one: unheard, it would take the
107
+ * process down, and node:http teaches 'aborted'/'close';
108
+ * 'close' — and never 'end', which would say the body is whole.
109
+ *
110
+ * It ended the stream with push(null) — 'end', a cut-off upload read as a
111
+ * complete one — and each caller decided the 'error' question for itself,
112
+ * one (h1_server) not at all, so a client vanishing mid-upload threw.
113
+ * A body that had already arrived in full is left as it is: 'aborted', and
114
+ * nothing to cut.
96
115
  */
97
116
  _abort(err) {
98
- if (this.complete) return;
117
+ if (this.aborted) return;
99
118
  this.aborted = true;
119
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
120
+ this.emit('aborted');
121
+ if (this.complete) return;
100
122
  this.complete = true;
101
- if (err) this.destroy(err);
102
- else this.push(null);
123
+ if (!err) { err = new Error('aborted'); err.code = 'ECONNRESET'; }
124
+ this.destroy(this.listenerCount('error') > 0 ? err : null);
103
125
  }
104
126
 
105
127
  setTimeout(msecs, callback) {
@@ -110,6 +132,32 @@ class IncomingMessage extends Readable {
110
132
  }
111
133
  return this;
112
134
  }
135
+
136
+ /**
137
+ * What destroy() by the application means on the wire, given by the
138
+ * transport that made this message: a request's body the server no longer
139
+ * wants (STOP_SENDING / RST_STREAM; over HTTP/1.1, the socket), a response
140
+ * the client gave up on mid-way. Not run when the stream itself ended it
141
+ * (_abort: the peer's reset, the connection gone); each hook decides what
142
+ * is left to stop (a body that arrived whole: nothing). destroy() was
143
+ * local only: the body kept coming, at full speed once its credit was
144
+ * released, into a message nobody read.
145
+ */
146
+ _onDestroy(fn) {
147
+ (this._destroyHooks || (this._destroyHooks = [])).push(fn);
148
+ }
149
+
150
+ _destroy(err, cb) {
151
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
152
+ var hooks = this._destroyHooks;
153
+ this._destroyHooks = null;
154
+ // Not after 'end': that is autoDestroy closing a body read to the end,
155
+ // not the application giving anything up.
156
+ if (hooks && !this.aborted && !this.readableEnded) {
157
+ for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
158
+ }
159
+ cb(err);
160
+ }
113
161
  }
114
162
 
115
163
 
@@ -128,7 +176,10 @@ class ServerResponse extends Writable {
128
176
  options = options || {};
129
177
 
130
178
  this.statusCode = 200;
131
- this.statusMessage = 'OK';
179
+ // Unset, as node:http has it: the standard reason phrase for whatever the
180
+ // status ends up being. It was 'OK', so a status set without one —
181
+ // res.statusCode = 404, writeHead(425, headers) — went out as '404 OK'.
182
+ this.statusMessage = undefined;
132
183
  this.headersSent = false;
133
184
  this.finished = false;
134
185
  this._headers = {};
@@ -144,9 +195,11 @@ class ServerResponse extends Writable {
144
195
  this.socket = options.socket || _makeSocketStub();
145
196
  this.connection = this.socket;
146
197
 
198
+ // 'close' comes from the Writable itself (autoDestroy), once, after
199
+ // 'finish' — node's order. It was also emitted here, from a 'finish'
200
+ // listener registered first: the application saw close, finish, close.
147
201
  this.on('finish', () => {
148
202
  this.finished = true;
149
- this.emit('close');
150
203
  });
151
204
 
152
205
  // Express does res.__proto__ = app.response (http.ServerResponse.prototype)
@@ -164,10 +217,49 @@ class ServerResponse extends Writable {
164
217
  this.end = this.end.bind(this);
165
218
  }
166
219
 
220
+ /**
221
+ * The peer is gone — the stream reset, the connection lost — while this
222
+ * response was still being written. As node:http: 'close', once; later
223
+ * writes are dropped without an error, so a handler mid-flight is not
224
+ * taken down, and a pipe() into the response stops. Without it a response
225
+ * whose connection died never closed, and a pipe() kept reading its source
226
+ * into a connection that was gone.
227
+ */
228
+ _abortResponse() {
229
+ if (this.writableFinished || this.destroyed) return;
230
+ this._aborted = true;
231
+ this.destroy();
232
+ }
233
+
234
+ /**
235
+ * res.destroy([err]) by the application, mid-response: the transport ends
236
+ * the stream so the client hears it — RESET_STREAM over HTTP/3, RST_STREAM
237
+ * over HTTP/2, the socket over HTTP/1.1 (as node:http) — with an internal
238
+ * error's code when there is an err (a failed pipeline), a cancel's
239
+ * otherwise. It was local only: the client waited for the rest of a
240
+ * response that would never come, and over HTTP/3 and HTTP/2 the stream
241
+ * stayed open until the connection closed. As node:http, the err is not
242
+ * re-emitted as 'error' to a response nobody listens to.
243
+ */
244
+ _onDestroy(fn) {
245
+ (this._destroyHooks || (this._destroyHooks = [])).push(fn);
246
+ }
247
+
248
+ _destroy(err, cb) {
249
+ var hooks = this._destroyHooks;
250
+ this._destroyHooks = null;
251
+ if (hooks && !this._aborted && !this.writableFinished) {
252
+ for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
253
+ }
254
+ cb(err && this.listenerCount('error') > 0 ? err : null);
255
+ }
256
+
167
257
  // ---- Header management (mirrors node:http) ----
168
258
 
169
259
  setHeader(name, value) {
170
- if (this.headersSent) throw new Error('Cannot set headers after they are sent to the client');
260
+ if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
261
+ validateHeaderName(name);
262
+ validateHeaderValue(name, value);
171
263
  var lower = name.toLowerCase();
172
264
  this._headers[lower] = value;
173
265
  this._headerNames[lower] = name;
@@ -193,13 +285,32 @@ class ServerResponse extends Writable {
193
285
  }
194
286
 
195
287
  removeHeader(name) {
196
- if (this.headersSent) throw new Error('Cannot remove headers after they are sent to the client');
288
+ if (this.headersSent) throw headersSentError('remove', this.req && this.req.httpVersion);
197
289
  var lower = name.toLowerCase();
198
290
  delete this._headers[lower];
199
291
  delete this._headerNames[lower];
200
292
  return this;
201
293
  }
202
294
 
295
+ /**
296
+ * node:http's res.addTrailers(headers): fields sent after the body — an
297
+ * object, or an array of [name, value] pairs. Over HTTP/1.1 they go in the
298
+ * last chunk (chunked responses only, as node); over HTTP/2 as a trailing
299
+ * HEADERS frame. The layers read res.trailers, but the method an application
300
+ * calls was missing: it threw a TypeError inside the handler, and over
301
+ * HTTP/2 that tore down the whole connection.
302
+ */
303
+ addTrailers(headers) {
304
+ if (!this.trailers) this.trailers = {};
305
+ var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
306
+ for (var i = 0; i < pairs.length; i++) {
307
+ var name = pairs[i][0], value = pairs[i][1];
308
+ validateHeaderName(name, 'Trailer name');
309
+ validateHeaderValue(name, value);
310
+ this.trailers[String(name).toLowerCase()] = value;
311
+ }
312
+ }
313
+
203
314
  // ---- writeHead ----
204
315
 
205
316
  writeHead(statusCode, statusMessage, headers) {
@@ -210,6 +321,7 @@ class ServerResponse extends Writable {
210
321
  statusMessage = undefined;
211
322
  }
212
323
 
324
+ validateStatus(statusCode, statusMessage);
213
325
  this.statusCode = statusCode;
214
326
  if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
215
327
 
@@ -259,18 +371,49 @@ class ServerResponse extends Writable {
259
371
 
260
372
  write(chunk, encoding, callback) {
261
373
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
374
+ this._wrote = true;
262
375
  return super.write(chunk, encoding, callback);
263
376
  }
264
377
 
265
378
  end(data, encoding, callback) {
266
379
  if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
267
380
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
381
+ // The whole body, when nothing was written before it and the head has
382
+ // not gone: its length, for HTTP/1.1 to send as Content-Length rather
383
+ // than chunk it (node:http's end() does the same). HTTP/2 and HTTP/3
384
+ // frame the body themselves and do not read it.
385
+ if (!this.headersSent && !this._wrote && !this.writableEnded) {
386
+ var len = data == null ? 0
387
+ : typeof data === 'string' ? Buffer.byteLength(data, encoding)
388
+ : data.byteLength;
389
+ if (typeof len === 'number') this._endLength = len; // else write() rejects it
390
+ }
268
391
  if (data != null) this.write(data, encoding);
269
392
  return super.end(callback);
270
393
  }
271
394
 
272
395
  writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
273
396
 
397
+ /**
398
+ * 103 Early Hints (RFC 8297) — node:http's response.writeEarlyHints(hints[,
399
+ * callback]). `hints` is a header object; `link` a string or an array of
400
+ * them. The protocol layer that made this response replaces this with the
401
+ * real thing (h1_server, h2_server, h3_server); this one, on a response
402
+ * nothing wired, sends nothing and still calls back.
403
+ */
404
+ writeEarlyHints(hints, callback) {
405
+ if (typeof callback === 'function') process.nextTick(callback);
406
+ }
407
+
408
+ /**
409
+ * The response's priority, { urgency, incremental } (RFC 9218). HTTP/2 and
410
+ * HTTP/3 replace this with the real thing (h2_server, h3_server); over
411
+ * HTTP/1.1 a connection carries one response at a time, so there is
412
+ * nothing to order, and it does nothing. It was missing there: the same
413
+ * handler, reached over HTTP/1.1, threw a TypeError.
414
+ */
415
+ setPriority(priority) {}
416
+
274
417
  setTimeout(msecs, callback) {
275
418
  if (callback) this.once('timeout', callback);
276
419
  return this;
@@ -282,12 +425,22 @@ class ServerResponse extends Writable {
282
425
  // ClientRequest — extends Writable
283
426
  //
284
427
  // Wraps outgoing HTTP request on the client side.
285
- // write() / pipe(req) buffers body data.
286
- // _final() signals body-complete so the transport can send.
428
+ // The body flows as it is written, as node:http's does: held here only
429
+ // until a transport takes the request (_attachSink — the first of the
430
+ // protocols tried to be ready to send), then handed to it, with its
431
+ // backpressure (write() false, 'drain'). What was handed is kept, up to
432
+ // REPLAY_MAX, so that a request another protocol must carry after all — the
433
+ // first failed before the server processed it — can be sent again whole.
287
434
  //
288
435
  // The 'response' event delivers an IncomingMessage.
289
436
  // ============================================================
290
437
 
438
+ // How much of a request body is kept, once handed to a transport, for
439
+ // sending again over another — what a request refused unprocessed (REFUSED_
440
+ // STREAM, H3_REQUEST_REJECTED, past a GOAWAY) or whose first protocol failed
441
+ // may need. Past it, the body streams without a copy.
442
+ var REPLAY_MAX = 64 * 1024;
443
+
291
444
  class ClientRequest extends Writable {
292
445
 
293
446
  constructor(options) {
@@ -309,6 +462,11 @@ class ClientRequest extends Writable {
309
462
 
310
463
  options = options || {};
311
464
 
465
+ // node:http throws these from http.request() itself: the path (no CR, LF,
466
+ // space or other control character — CVE-2018-12116 was a request split
467
+ // through it) and the method (a token).
468
+ validateRequestTarget(options.path, options.method);
469
+
312
470
  this.method = (options.method || 'GET').toUpperCase();
313
471
  this.path = options.path || '/';
314
472
  this.host = options.hostname || options.host || 'localhost';
@@ -327,8 +485,17 @@ class ClientRequest extends Writable {
327
485
  for (var name in options.headers) this.setHeader(name, options.headers[name]);
328
486
  }
329
487
 
330
- // Body buffer — accumulated via write/pipe, sent when _final fires
488
+ // The body: held until a transport takes the request, then streamed to it.
331
489
  this._bodyChunks = [];
490
+ this._bodyBuffered = 0;
491
+ this._pendingWriteCb = null; // a write held for room (before a transport)
492
+ this._sink = null; // the transport request carrying the body
493
+ this._sinkWaitCb = null; // a write waiting for the sink's 'drain'
494
+ this._replay = []; // what the sink was handed, while ≤ REPLAY_MAX
495
+ this._replayBytes = 0;
496
+ this._replayable = true;
497
+ this._trailers = null; // addTrailers()
498
+ this._priority = null; // setPriority()
332
499
 
333
500
  // H3 internals — set by unified client after protocol selection
334
501
  this._h3 = null;
@@ -346,11 +513,49 @@ class ClientRequest extends Writable {
346
513
 
347
514
  this._timeoutTimer = null;
348
515
  this._timeoutMs = 0;
516
+
517
+ // The response, once there is one (node:http's req.res), and how to stop
518
+ // each transport attempt that carries this request (see _destroy).
519
+ this.res = null;
520
+ this._cancels = [];
521
+ }
522
+
523
+ // ---- Cancellation ----
524
+ //
525
+ // destroy() / abort() stop the exchange where it is carried, as node:http's
526
+ // do by destroying the socket. Without this they only marked the request:
527
+ // the transport went on, and the response kept downloading in the
528
+ // background. Each transport attempt registers how to stop itself — over
529
+ // HTTP/3 and HTTP/2 that is the request's stream, not the (shared)
530
+ // connection; over HTTP/1.1, the socket, which cannot be reused mid-exchange.
531
+
532
+ _onCancel(fn) {
533
+ if (this.destroyed) { fn(); return; }
534
+ this._cancels.push(fn);
535
+ }
536
+
537
+ // node:http: destroyed before a response, the request fails with 'socket
538
+ // hang up' (ECONNRESET) unless given its own error.
539
+ destroy(err) {
540
+ if (this.destroyed) return this;
541
+ if (!err && !this.res && !this.aborted) { err = new Error('socket hang up'); err.code = 'ECONNRESET'; }
542
+ return super.destroy(err);
543
+ }
544
+
545
+ _destroy(err, cb) {
546
+ var cancels = this._cancels;
547
+ this._cancels = [];
548
+ for (var i = 0; i < cancels.length; i++) { try { cancels[i](); } catch (e) {} }
549
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
550
+ if (this.res && !this.res.complete) this.res._abort(err && err.code !== 'ECONNRESET' ? err : null);
551
+ cb(err);
349
552
  }
350
553
 
351
554
  // ---- Header management ----
352
555
 
353
556
  setHeader(name, value) {
557
+ validateHeaderName(name);
558
+ validateHeaderValue(name, value);
354
559
  var lower = name.toLowerCase();
355
560
  this._headers[lower] = value;
356
561
  this._headerNames[lower] = name;
@@ -388,16 +593,93 @@ class ClientRequest extends Writable {
388
593
  if (Buffer.isBuffer(chunk)) {
389
594
  chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
390
595
  }
596
+ if (this._sink) { this._toSink(chunk, callback); return; }
597
+ // No transport yet (still connecting): held — and the writer told to wait
598
+ // past the high-water mark, as a socket not yet connected would.
391
599
  this._bodyChunks.push(chunk);
392
- callback();
600
+ this._bodyBuffered += chunk.byteLength;
601
+ if (this._bodyBuffered < this.writableHighWaterMark) callback();
602
+ else this._pendingWriteCb = callback;
393
603
  }
394
604
 
395
605
  _final(callback) {
396
606
  this.finished = true;
397
- this.emit('_bodyReady');
607
+ if (this._sink) this._sink.end(null, this._trailers);
398
608
  callback();
399
609
  }
400
610
 
611
+ // node:http's addTrailers(): fields sent after the body (a HEADERS frame
612
+ // over HTTP/2 and HTTP/3; with chunked encoding over HTTP/1.1). An object,
613
+ // or a list of [name, value] pairs.
614
+ addTrailers(headers) {
615
+ var t = this._trailers || (this._trailers = {});
616
+ var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
617
+ for (var i = 0; i < pairs.length; i++) {
618
+ var name = pairs[i][0], value = pairs[i][1];
619
+ validateHeaderName(name);
620
+ validateHeaderValue(name, value);
621
+ t[String(name).toLowerCase()] = value;
622
+ }
623
+ }
624
+
625
+ // The request's priority (RFC 9218): { urgency: 0-7 (3), incremental
626
+ // (false) } — the `priority` header while the request has not gone, a
627
+ // PRIORITY_UPDATE after it (HTTP/2, HTTP/3), as res.setPriority is the
628
+ // server's. Normalized the same way.
629
+ setPriority(p) {
630
+ this._priority = responsePriority(p);
631
+ if (this._sink && typeof this._sink.setPriority === 'function') this._sink.setPriority(this._priority);
632
+ return this;
633
+ }
634
+
635
+ // ---- The transport that carries the body (client.js) ----
636
+
637
+ // `sink` (a transport's request: write → more?, 'drain', end(chunk,
638
+ // trailers)) takes the request: what is held goes to it, and from now on
639
+ // every write.
640
+ _attachSink(sink) {
641
+ var self = this;
642
+ this._sink = sink;
643
+ if (this._priority && typeof sink.setPriority === 'function') sink.setPriority(this._priority);
644
+ sink.on('drain', function () {
645
+ if (self._sink !== sink || !self._sinkWaitCb) return;
646
+ var cb = self._sinkWaitCb; self._sinkWaitCb = null; cb();
647
+ });
648
+ var held = this._bodyChunks, more = true;
649
+ this._bodyChunks = []; this._bodyBuffered = 0;
650
+ for (var i = 0; i < held.length; i++) { this._remember(held[i]); more = sink.write(held[i]); }
651
+ if (this._pendingWriteCb) {
652
+ var cb = this._pendingWriteCb; this._pendingWriteCb = null;
653
+ if (more) cb(); else this._sinkWaitCb = cb;
654
+ }
655
+ if (this.finished) sink.end(null, this._trailers);
656
+ }
657
+
658
+ // The transport failed before the server processed the request: the body
659
+ // back here, whole, for the next — or false if it cannot be (more than
660
+ // REPLAY_MAX was handed over: not kept).
661
+ _detachSink() {
662
+ if (!this._replayable) return false;
663
+ this._sink = null;
664
+ this._bodyChunks = this._replay; this._bodyBuffered = this._replayBytes;
665
+ this._replay = []; this._replayBytes = 0;
666
+ if (this._sinkWaitCb) { this._pendingWriteCb = this._sinkWaitCb; this._sinkWaitCb = null; }
667
+ return true;
668
+ }
669
+
670
+ _toSink(chunk, callback) {
671
+ this._remember(chunk);
672
+ if (this._sink.write(chunk)) callback();
673
+ else this._sinkWaitCb = callback;
674
+ }
675
+
676
+ _remember(chunk) {
677
+ if (!this._replayable) return;
678
+ this._replayBytes += chunk.byteLength;
679
+ if (this._replayBytes > REPLAY_MAX) { this._replayable = false; this._replay = []; this._replayBytes = 0; return; }
680
+ this._replay.push(chunk);
681
+ }
682
+
401
683
  // ---- Convenience overrides ----
402
684
 
403
685
  write(chunk, encoding, callback) {
@@ -412,9 +694,13 @@ class ClientRequest extends Writable {
412
694
  return super.end(callback);
413
695
  }
414
696
 
697
+ // node:http: 'abort', then 'close' — no error.
415
698
  abort() {
699
+ if (this.aborted || this.destroyed) return;
416
700
  this.aborted = true;
417
- this.destroy(new Error('Request aborted'));
701
+ var self = this;
702
+ process.nextTick(function () { self.emit('abort'); });
703
+ this.destroy();
418
704
  }
419
705
 
420
706
  setTimeout(msecs, callback) {
@@ -426,24 +712,6 @@ class ClientRequest extends Writable {
426
712
  }
427
713
  return this;
428
714
  }
429
-
430
- /**
431
- * Collect all buffered body chunks into one Uint8Array.
432
- * Returns null if nothing was written (GET / HEAD).
433
- */
434
- _getBody() {
435
- if (this._bodyChunks.length === 0) return null;
436
- if (this._bodyChunks.length === 1) return this._bodyChunks[0];
437
- var total = 0;
438
- for (var i = 0; i < this._bodyChunks.length; i++) total += this._bodyChunks[i].byteLength;
439
- var merged = new Uint8Array(total);
440
- var off = 0;
441
- for (var i = 0; i < this._bodyChunks.length; i++) {
442
- merged.set(this._bodyChunks[i], off);
443
- off += this._bodyChunks[i].byteLength;
444
- }
445
- return merged;
446
- }
447
715
  }
448
716
 
449
717
 
@@ -452,6 +720,64 @@ class ClientRequest extends Writable {
452
720
  // Must be a real Stream so Node's eos() / Express don't crash.
453
721
  // ============================================================
454
722
 
723
+ /**
724
+ * Make an Express application (or anything built the same way) run on these
725
+ * streams.
726
+ *
727
+ * Express does not use the req and res it is handed as they are: on every
728
+ * request it swaps their prototype for its own (Object.setPrototypeOf(req,
729
+ * app.request)), and that chain ends in node:http's IncomingMessage and
730
+ * ServerResponse. Every method of these classes is gone after that — a
731
+ * request body arrived through _pushData, which no longer existed, and the
732
+ * server fell over on the first POST. The response survived only because its
733
+ * methods are also bound onto each instance (see the constructor), and even
734
+ * so it ran on node:http's getters for its state.
735
+ *
736
+ * So the chain is re-parented instead, once per application:
737
+ *
738
+ * before app.response → express's response methods → http.ServerResponse
739
+ * after app.response → a copy of those methods → quico ServerResponse
740
+ *
741
+ * Express's methods — req.get, res.json, res.send, res.cookie,
742
+ * express.static — sit on top and call the ones below, which are now ours:
743
+ * the same object, on HTTP/1.1, HTTP/2 and HTTP/3.
744
+ *
745
+ * A COPY, because the object holding Express's methods is shared by every
746
+ * Express application in the process: re-parenting it would move them all,
747
+ * including any served by node:http. The copy belongs to this application
748
+ * alone. A sub-application inherits from its parent's objects when it is
749
+ * mounted, so the root is the only one that needs it.
750
+ *
751
+ * Recognised by shape — a function with `request` and `response` objects —
752
+ * not by name; left alone when already adopted or not that shape. Returns
753
+ * the handler, for chaining.
754
+ */
755
+ function adoptApp(handler) {
756
+ if (typeof handler !== 'function') return handler;
757
+ reparent(handler.request, IncomingMessage);
758
+ reparent(handler.response, ServerResponse);
759
+ return handler;
760
+ }
761
+
762
+ // Put `Base` under obj's prototype chain in place of the class it ends in: the
763
+ // objects between obj and the first class prototype (one owning a
764
+ // `constructor`) are copied, the last copy inheriting from Base.prototype.
765
+ function reparent(obj, Base) {
766
+ if (!obj || typeof obj !== 'object' || obj instanceof Base) return;
767
+ var between = [];
768
+ var p = Object.getPrototypeOf(obj);
769
+ while (p && !Object.prototype.hasOwnProperty.call(p, 'constructor')) {
770
+ between.push(p);
771
+ p = Object.getPrototypeOf(p);
772
+ }
773
+ if (!p || p === Object.prototype) return; // not a class chain: not ours to change
774
+ var top = Base.prototype;
775
+ for (var i = between.length - 1; i >= 0; i--) {
776
+ top = Object.create(top, Object.getOwnPropertyDescriptors(between[i]));
777
+ }
778
+ Object.setPrototypeOf(obj, top);
779
+ }
780
+
455
781
  function _makeSocketStub() {
456
782
  var sock = new Duplex({
457
783
  read(_size) { /* no-op — no data from QUIC socket */ },
@@ -482,4 +808,4 @@ function _makeSocketStub() {
482
808
  }
483
809
 
484
810
 
485
- export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub };
811
+ export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub, adoptApp };