quico 0.4.0 → 0.8.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/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,34 +53,59 @@ 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();
58
61
  this.connection = this.socket;
62
+ this.client = this.socket; // node:http's older alias
63
+ // node:http's: true on a request handed to 'upgrade' (an HTTP/1.1
64
+ // Upgrade, an extended CONNECT over HTTP/2 and HTTP/3).
65
+ this.upgrade = false;
59
66
 
60
67
  // H3 stream id (internal)
61
68
  this._streamId = options.streamId != null ? options.streamId : null;
62
69
  this._timeoutTimer = null;
63
70
 
64
- // Build rawHeaders
65
- for (var name in this.headers) {
66
- this.rawHeaders.push(name, String(this.headers[name]));
71
+ // Build rawHeaders: one name and value per field line, as node:http has
72
+ // them — the field lines as they came when the decoder kept them
73
+ // (validateFieldSection's _rawHeaders); otherwise from the map, a list
74
+ // (Set-Cookie) one entry per value. String(list) made two cookies one
75
+ // "a=1,b=2".
76
+ var rawList = this.headers._rawHeaders;
77
+ if (Array.isArray(rawList)) {
78
+ for (var ri = 0; ri < rawList.length; ri++) this.rawHeaders.push(rawList[ri]);
79
+ } else {
80
+ for (var name in this.headers) {
81
+ var hv = this.headers[name];
82
+ if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) this.rawHeaders.push(name, String(hv[hi])); }
83
+ else this.rawHeaders.push(name, String(hv));
84
+ }
67
85
  }
68
86
  }
69
87
 
88
+ // node:http's headersDistinct / trailersDistinct: each name (lower case)
89
+ // with every value it came with, as an array, nothing joined — from the
90
+ // field lines as they arrived (rawHeaders, rawTrailers).
91
+ get headersDistinct() { return distinctFields(this.rawHeaders); }
92
+ get trailersDistinct() { return distinctFields(this.rawTrailers); }
93
+
70
94
  // Readable requires _read — data is pushed externally, so no-op.
71
95
  _read(_size) { /* no-op */ }
72
96
 
73
97
  /**
74
98
  * Called by H3 layer when a body chunk arrives.
75
99
  */
100
+ // push()'s answer: false once the buffer is full — the caller stops
101
+ // reading until _read asks for more.
76
102
  _pushData(chunk) {
77
- if (this.complete) return;
103
+ if (this.complete) return true;
78
104
  if (typeof chunk === 'string') chunk = Buffer.from(chunk);
79
105
  if (chunk instanceof Uint8Array && !Buffer.isBuffer(chunk)) {
80
106
  chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
81
107
  }
82
- this.push(chunk);
108
+ return this.push(chunk);
83
109
  }
84
110
 
85
111
  /**
@@ -92,14 +118,31 @@ class IncomingMessage extends Readable {
92
118
  }
93
119
 
94
120
  /**
95
- * Abort the stream (error / timeout / reset).
121
+ * The exchange ended before its body did — the peer reset the stream or
122
+ * went away, or (client side) req.destroy(). The one place this is decided,
123
+ * as node:http's IncomingMessage does it:
124
+ *
125
+ * 'aborted' — always, once;
126
+ * 'error' — ('aborted', ECONNRESET, or the cause given) only to a
127
+ * listener that asked for one: unheard, it would take the
128
+ * process down, and node:http teaches 'aborted'/'close';
129
+ * 'close' — and never 'end', which would say the body is whole.
130
+ *
131
+ * It ended the stream with push(null) — 'end', a cut-off upload read as a
132
+ * complete one — and each caller decided the 'error' question for itself,
133
+ * one (h1_server) not at all, so a client vanishing mid-upload threw.
134
+ * A body that had already arrived in full is left as it is: 'aborted', and
135
+ * nothing to cut.
96
136
  */
97
137
  _abort(err) {
98
- if (this.complete) return;
138
+ if (this.aborted) return;
99
139
  this.aborted = true;
140
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
141
+ this.emit('aborted');
142
+ if (this.complete) return;
100
143
  this.complete = true;
101
- if (err) this.destroy(err);
102
- else this.push(null);
144
+ if (!err) { err = new Error('aborted'); err.code = 'ECONNRESET'; }
145
+ this.destroy(this.listenerCount('error') > 0 ? err : null);
103
146
  }
104
147
 
105
148
  setTimeout(msecs, callback) {
@@ -110,6 +153,32 @@ class IncomingMessage extends Readable {
110
153
  }
111
154
  return this;
112
155
  }
156
+
157
+ /**
158
+ * What destroy() by the application means on the wire, given by the
159
+ * transport that made this message: a request's body the server no longer
160
+ * wants (STOP_SENDING / RST_STREAM; over HTTP/1.1, the socket), a response
161
+ * the client gave up on mid-way. Not run when the stream itself ended it
162
+ * (_abort: the peer's reset, the connection gone); each hook decides what
163
+ * is left to stop (a body that arrived whole: nothing). destroy() was
164
+ * local only: the body kept coming, at full speed once its credit was
165
+ * released, into a message nobody read.
166
+ */
167
+ _onDestroy(fn) {
168
+ (this._destroyHooks || (this._destroyHooks = [])).push(fn);
169
+ }
170
+
171
+ _destroy(err, cb) {
172
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
173
+ var hooks = this._destroyHooks;
174
+ this._destroyHooks = null;
175
+ // Not after 'end': that is autoDestroy closing a body read to the end,
176
+ // not the application giving anything up.
177
+ if (hooks && !this.aborted && !this.readableEnded) {
178
+ for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
179
+ }
180
+ cb(err);
181
+ }
113
182
  }
114
183
 
115
184
 
@@ -128,11 +197,19 @@ class ServerResponse extends Writable {
128
197
  options = options || {};
129
198
 
130
199
  this.statusCode = 200;
131
- this.statusMessage = 'OK';
200
+ // Unset, as node:http has it: the standard reason phrase for whatever the
201
+ // status ends up being. It was 'OK', so a status set without one —
202
+ // res.statusCode = 404, writeHead(425, headers) — went out as '404 OK'.
203
+ this.statusMessage = undefined;
132
204
  this.headersSent = false;
133
205
  this.finished = false;
134
206
  this._headers = {};
135
207
  this._headerNames = {}; // lowercase → original-case
208
+ // node:http's: a Date header unless false (RFC 9110 §6.6.1); and, when
209
+ // true, a body that does not match its Content-Length throws.
210
+ this.sendDate = true;
211
+ this.strictContentLength = false;
212
+ this._bodyBytes = 0;
136
213
 
137
214
  // H3 references (set by server when creating the response)
138
215
  this._streamId = options.streamId != null ? options.streamId : null;
@@ -144,9 +221,11 @@ class ServerResponse extends Writable {
144
221
  this.socket = options.socket || _makeSocketStub();
145
222
  this.connection = this.socket;
146
223
 
224
+ // 'close' comes from the Writable itself (autoDestroy), once, after
225
+ // 'finish' — node's order. It was also emitted here, from a 'finish'
226
+ // listener registered first: the application saw close, finish, close.
147
227
  this.on('finish', () => {
148
228
  this.finished = true;
149
- this.emit('close');
150
229
  });
151
230
 
152
231
  // Express does res.__proto__ = app.response (http.ServerResponse.prototype)
@@ -157,6 +236,9 @@ class ServerResponse extends Writable {
157
236
  this.getHeaderNames = this.getHeaderNames.bind(this);
158
237
  this.hasHeader = this.hasHeader.bind(this);
159
238
  this.removeHeader = this.removeHeader.bind(this);
239
+ this.appendHeader = this.appendHeader.bind(this);
240
+ this.setHeaders = this.setHeaders.bind(this);
241
+ this.getRawHeaderNames = this.getRawHeaderNames.bind(this);
160
242
  this.writeHead = this.writeHead.bind(this);
161
243
  this.flushHeaders = this.flushHeaders.bind(this);
162
244
  this._flushHeaders = this._flushHeaders.bind(this);
@@ -164,10 +246,49 @@ class ServerResponse extends Writable {
164
246
  this.end = this.end.bind(this);
165
247
  }
166
248
 
249
+ /**
250
+ * The peer is gone — the stream reset, the connection lost — while this
251
+ * response was still being written. As node:http: 'close', once; later
252
+ * writes are dropped without an error, so a handler mid-flight is not
253
+ * taken down, and a pipe() into the response stops. Without it a response
254
+ * whose connection died never closed, and a pipe() kept reading its source
255
+ * into a connection that was gone.
256
+ */
257
+ _abortResponse() {
258
+ if (this.writableFinished || this.destroyed) return;
259
+ this._aborted = true;
260
+ this.destroy();
261
+ }
262
+
263
+ /**
264
+ * res.destroy([err]) by the application, mid-response: the transport ends
265
+ * the stream so the client hears it — RESET_STREAM over HTTP/3, RST_STREAM
266
+ * over HTTP/2, the socket over HTTP/1.1 (as node:http) — with an internal
267
+ * error's code when there is an err (a failed pipeline), a cancel's
268
+ * otherwise. It was local only: the client waited for the rest of a
269
+ * response that would never come, and over HTTP/3 and HTTP/2 the stream
270
+ * stayed open until the connection closed. As node:http, the err is not
271
+ * re-emitted as 'error' to a response nobody listens to.
272
+ */
273
+ _onDestroy(fn) {
274
+ (this._destroyHooks || (this._destroyHooks = [])).push(fn);
275
+ }
276
+
277
+ _destroy(err, cb) {
278
+ var hooks = this._destroyHooks;
279
+ this._destroyHooks = null;
280
+ if (hooks && !this._aborted && !this.writableFinished) {
281
+ for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
282
+ }
283
+ cb(err && this.listenerCount('error') > 0 ? err : null);
284
+ }
285
+
167
286
  // ---- Header management (mirrors node:http) ----
168
287
 
169
288
  setHeader(name, value) {
170
- if (this.headersSent) throw new Error('Cannot set headers after they are sent to the client');
289
+ if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
290
+ validateHeaderName(name);
291
+ validateHeaderValue(name, value);
171
292
  var lower = name.toLowerCase();
172
293
  this._headers[lower] = value;
173
294
  this._headerNames[lower] = name;
@@ -178,8 +299,10 @@ class ServerResponse extends Writable {
178
299
  return this._headers[name.toLowerCase()];
179
300
  }
180
301
 
302
+ // An object with no prototype, as node:http's: a header named
303
+ // "constructor" or "__proto__" is a header, not a property it inherits.
181
304
  getHeaders() {
182
- var out = {};
305
+ var out = Object.create(null);
183
306
  for (var k in this._headers) out[k] = this._headers[k];
184
307
  return out;
185
308
  }
@@ -188,18 +311,79 @@ class ServerResponse extends Writable {
188
311
  return Object.keys(this._headers);
189
312
  }
190
313
 
314
+ // node:http's: the names as they were set, in their case.
315
+ getRawHeaderNames() {
316
+ var out = [];
317
+ for (var k in this._headers) out.push(this._headerNames[k] || k);
318
+ return out;
319
+ }
320
+
321
+ // node:http's: a value added to a header's (both kept), not in its place.
322
+ appendHeader(name, value) {
323
+ if (this.headersSent) throw headersSentError('append', this.req && this.req.httpVersion);
324
+ return this._appendHeader(name, value);
325
+ }
326
+
327
+ // node:http's: several at once, from a Headers or a Map — each replacing
328
+ // that name's value (Set-Cookie: every one of a Headers' cookies).
329
+ setHeaders(headers) {
330
+ if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
331
+ var isHeaders = typeof Headers !== 'undefined' && headers instanceof Headers;
332
+ if (!isHeaders && !(headers instanceof Map)) {
333
+ var e = new TypeError('The "headers" argument must be an instance of Headers or Map');
334
+ e.code = 'ERR_INVALID_ARG_TYPE';
335
+ throw e;
336
+ }
337
+ var self = this, cookies = null;
338
+ headers.forEach(function (value, name) {
339
+ if (isHeaders && String(name).toLowerCase() === 'set-cookie') { cookies = headers.getSetCookie(); return; }
340
+ self.setHeader(name, value);
341
+ });
342
+ if (cookies) this.setHeader('set-cookie', cookies);
343
+ return this;
344
+ }
345
+
191
346
  hasHeader(name) {
192
347
  return name.toLowerCase() in this._headers;
193
348
  }
194
349
 
195
350
  removeHeader(name) {
196
- if (this.headersSent) throw new Error('Cannot remove headers after they are sent to the client');
351
+ if (this.headersSent) throw headersSentError('remove', this.req && this.req.httpVersion);
197
352
  var lower = name.toLowerCase();
198
353
  delete this._headers[lower];
199
354
  delete this._headerNames[lower];
200
355
  return this;
201
356
  }
202
357
 
358
+ // A value added to what a name has (kept as a list), not in its place.
359
+ _appendHeader(name, value) {
360
+ var lower = String(name).toLowerCase();
361
+ if (!(lower in this._headers)) return this.setHeader(name, value);
362
+ validateHeaderName(name);
363
+ validateHeaderValue(name, value);
364
+ this._headers[lower] = [].concat(this._headers[lower], value);
365
+ return this;
366
+ }
367
+
368
+ /**
369
+ * node:http's res.addTrailers(headers): fields sent after the body — an
370
+ * object, or an array of [name, value] pairs. Over HTTP/1.1 they go in the
371
+ * last chunk (chunked responses only, as node); over HTTP/2 as a trailing
372
+ * HEADERS frame. The layers read res.trailers, but the method an application
373
+ * calls was missing: it threw a TypeError inside the handler, and over
374
+ * HTTP/2 that tore down the whole connection.
375
+ */
376
+ addTrailers(headers) {
377
+ if (!this.trailers) this.trailers = {};
378
+ var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
379
+ for (var i = 0; i < pairs.length; i++) {
380
+ var name = pairs[i][0], value = pairs[i][1];
381
+ validateHeaderName(name, 'Trailer name');
382
+ validateHeaderValue(name, value);
383
+ this.trailers[String(name).toLowerCase()] = value;
384
+ }
385
+ }
386
+
203
387
  // ---- writeHead ----
204
388
 
205
389
  writeHead(statusCode, statusMessage, headers) {
@@ -210,10 +394,26 @@ class ServerResponse extends Writable {
210
394
  statusMessage = undefined;
211
395
  }
212
396
 
397
+ validateStatus(statusCode, statusMessage);
213
398
  this.statusCode = statusCode;
214
399
  if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
215
400
 
216
- if (typeof headers === 'object' && headers !== null) {
401
+ // node:http's three forms: an object; a flat array [name, value, name,
402
+ // value]; an array of [name, value] pairs. The arrays may repeat a name
403
+ // and every value is kept (two Set-Cookie lines). An array went through
404
+ // for…in: headers named "0", "1", … — over HTTP/3 the response failed.
405
+ if (Array.isArray(headers)) {
406
+ var tuples = headers.length > 0 && Array.isArray(headers[0]);
407
+ if (!tuples && headers.length % 2 !== 0) {
408
+ var bad = new TypeError('The argument \'headers\' is invalid: an array of names and values must have an even length');
409
+ bad.code = 'ERR_INVALID_ARG_VALUE';
410
+ throw bad;
411
+ }
412
+ for (var i = 0; i < headers.length; i += tuples ? 1 : 2) {
413
+ var hn = tuples ? headers[i][0] : headers[i], hv = tuples ? headers[i][1] : headers[i + 1];
414
+ if (hn) this._appendHeader(hn, hv);
415
+ }
416
+ } else if (typeof headers === 'object' && headers !== null) {
217
417
  for (var name in headers) this.setHeader(name, headers[name]);
218
418
  }
219
419
 
@@ -259,18 +459,95 @@ class ServerResponse extends Writable {
259
459
 
260
460
  write(chunk, encoding, callback) {
261
461
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
462
+ this._wrote = true;
463
+ this._countBody(chunk, encoding, false);
262
464
  return super.write(chunk, encoding, callback);
263
465
  }
264
466
 
467
+ // strictContentLength (node:http's): the body may not go past its
468
+ // Content-Length, nor end short of it — ERR_HTTP_CONTENT_LENGTH_MISMATCH,
469
+ // thrown from the write() or end() that shows it.
470
+ // Counted only once checked: a write that threw sent nothing.
471
+ _countBody(chunk, encoding, ending) {
472
+ var total = this._bodyBytes;
473
+ if (chunk != null && !this.writableEnded) {
474
+ total += typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : (chunk.byteLength || 0);
475
+ }
476
+ var declared = this.strictContentLength ? this.getHeader('content-length') : undefined;
477
+ if (declared !== undefined && !Array.isArray(declared) && !(this.req && this.req.method === 'HEAD') && Number(declared) >= 0) {
478
+ declared = Number(declared);
479
+ if (total > declared || (ending && total !== declared)) {
480
+ var e = new Error('Response body\'s content-length of ' + total + ' byte(s) does not match the content-length of ' + declared + ' byte(s) set in header');
481
+ e.code = 'ERR_HTTP_CONTENT_LENGTH_MISMATCH';
482
+ throw e;
483
+ }
484
+ }
485
+ this._bodyBytes = total;
486
+ }
487
+
265
488
  end(data, encoding, callback) {
266
489
  if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
267
490
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
268
- if (data != null) this.write(data, encoding);
269
- return super.end(callback);
491
+ if (!this.writableEnded) {
492
+ if (data != null) this._countBody(data, encoding, true);
493
+ else this._countBody(null, null, true);
494
+ }
495
+ // The whole body, when nothing was written before it and the head has
496
+ // not gone: its length, for HTTP/1.1 to send as Content-Length rather
497
+ // than chunk it (node:http's end() does the same). HTTP/2 and HTTP/3
498
+ // frame the body themselves and do not read it.
499
+ if (!this.headersSent && !this._wrote && !this.writableEnded) {
500
+ var len = data == null ? 0
501
+ : typeof data === 'string' ? Buffer.byteLength(data, encoding)
502
+ : data.byteLength;
503
+ if (typeof len === 'number') this._endLength = len; // else write() rejects it
504
+ }
505
+ if (data != null) this._writeCounted(data, encoding);
506
+ // The callback runs at 'finish', after the listeners already on it — as
507
+ // node:http's end(cb), a once('finish'). Given to Writable's end() it
508
+ // ran before them.
509
+ if (typeof callback === 'function') {
510
+ if (this.writableFinished) process.nextTick(callback);
511
+ else this.once('finish', callback);
512
+ }
513
+ super.end();
514
+ return this;
515
+ }
516
+
517
+ // The last write, its bytes already counted by end().
518
+ _writeCounted(data, encoding) {
519
+ this._wrote = true;
520
+ return super.write(data, encoding);
270
521
  }
271
522
 
272
523
  writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
273
524
 
525
+ /**
526
+ * 103 Early Hints (RFC 8297) — node:http's response.writeEarlyHints(hints[,
527
+ * callback]). `hints` is a header object; `link` a string or an array of
528
+ * them. The protocol layer that made this response replaces this with the
529
+ * real thing (h1_server, h2_server, h3_server); this one, on a response
530
+ * nothing wired, sends nothing and still calls back.
531
+ */
532
+ writeEarlyHints(hints, callback) {
533
+ if (typeof callback === 'function') process.nextTick(callback);
534
+ }
535
+
536
+ /**
537
+ * 102 Processing — node:http's response.writeProcessing(). Replaced by the
538
+ * layer that made the response, as writeEarlyHints is; here, nothing.
539
+ */
540
+ writeProcessing() {}
541
+
542
+ /**
543
+ * The response's priority, { urgency, incremental } (RFC 9218). HTTP/2 and
544
+ * HTTP/3 replace this with the real thing (h2_server, h3_server); over
545
+ * HTTP/1.1 a connection carries one response at a time, so there is
546
+ * nothing to order, and it does nothing. It was missing there: the same
547
+ * handler, reached over HTTP/1.1, threw a TypeError.
548
+ */
549
+ setPriority(priority) {}
550
+
274
551
  setTimeout(msecs, callback) {
275
552
  if (callback) this.once('timeout', callback);
276
553
  return this;
@@ -282,12 +559,22 @@ class ServerResponse extends Writable {
282
559
  // ClientRequest — extends Writable
283
560
  //
284
561
  // 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.
562
+ // The body flows as it is written, as node:http's does: held here only
563
+ // until a transport takes the request (_attachSink — the first of the
564
+ // protocols tried to be ready to send), then handed to it, with its
565
+ // backpressure (write() false, 'drain'). What was handed is kept, up to
566
+ // REPLAY_MAX, so that a request another protocol must carry after all — the
567
+ // first failed before the server processed it — can be sent again whole.
287
568
  //
288
569
  // The 'response' event delivers an IncomingMessage.
289
570
  // ============================================================
290
571
 
572
+ // How much of a request body is kept, once handed to a transport, for
573
+ // sending again over another — what a request refused unprocessed (REFUSED_
574
+ // STREAM, H3_REQUEST_REJECTED, past a GOAWAY) or whose first protocol failed
575
+ // may need. Past it, the body streams without a copy.
576
+ var REPLAY_MAX = 64 * 1024;
577
+
291
578
  class ClientRequest extends Writable {
292
579
 
293
580
  constructor(options) {
@@ -309,6 +596,11 @@ class ClientRequest extends Writable {
309
596
 
310
597
  options = options || {};
311
598
 
599
+ // node:http throws these from http.request() itself: the path (no CR, LF,
600
+ // space or other control character — CVE-2018-12116 was a request split
601
+ // through it) and the method (a token).
602
+ validateRequestTarget(options.path, options.method);
603
+
312
604
  this.method = (options.method || 'GET').toUpperCase();
313
605
  this.path = options.path || '/';
314
606
  this.host = options.hostname || options.host || 'localhost';
@@ -327,8 +619,17 @@ class ClientRequest extends Writable {
327
619
  for (var name in options.headers) this.setHeader(name, options.headers[name]);
328
620
  }
329
621
 
330
- // Body buffer — accumulated via write/pipe, sent when _final fires
622
+ // The body: held until a transport takes the request, then streamed to it.
331
623
  this._bodyChunks = [];
624
+ this._bodyBuffered = 0;
625
+ this._pendingWriteCb = null; // a write held for room (before a transport)
626
+ this._sink = null; // the transport request carrying the body
627
+ this._sinkWaitCb = null; // a write waiting for the sink's 'drain'
628
+ this._replay = []; // what the sink was handed, while ≤ REPLAY_MAX
629
+ this._replayBytes = 0;
630
+ this._replayable = true;
631
+ this._trailers = null; // addTrailers()
632
+ this._priority = null; // setPriority()
332
633
 
333
634
  // H3 internals — set by unified client after protocol selection
334
635
  this._h3 = null;
@@ -346,11 +647,49 @@ class ClientRequest extends Writable {
346
647
 
347
648
  this._timeoutTimer = null;
348
649
  this._timeoutMs = 0;
650
+
651
+ // The response, once there is one (node:http's req.res), and how to stop
652
+ // each transport attempt that carries this request (see _destroy).
653
+ this.res = null;
654
+ this._cancels = [];
655
+ }
656
+
657
+ // ---- Cancellation ----
658
+ //
659
+ // destroy() / abort() stop the exchange where it is carried, as node:http's
660
+ // do by destroying the socket. Without this they only marked the request:
661
+ // the transport went on, and the response kept downloading in the
662
+ // background. Each transport attempt registers how to stop itself — over
663
+ // HTTP/3 and HTTP/2 that is the request's stream, not the (shared)
664
+ // connection; over HTTP/1.1, the socket, which cannot be reused mid-exchange.
665
+
666
+ _onCancel(fn) {
667
+ if (this.destroyed) { fn(); return; }
668
+ this._cancels.push(fn);
669
+ }
670
+
671
+ // node:http: destroyed before a response, the request fails with 'socket
672
+ // hang up' (ECONNRESET) unless given its own error.
673
+ destroy(err) {
674
+ if (this.destroyed) return this;
675
+ if (!err && !this.res && !this.aborted) { err = new Error('socket hang up'); err.code = 'ECONNRESET'; }
676
+ return super.destroy(err);
677
+ }
678
+
679
+ _destroy(err, cb) {
680
+ var cancels = this._cancels;
681
+ this._cancels = [];
682
+ for (var i = 0; i < cancels.length; i++) { try { cancels[i](); } catch (e) {} }
683
+ if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
684
+ if (this.res && !this.res.complete) this.res._abort(err && err.code !== 'ECONNRESET' ? err : null);
685
+ cb(err);
349
686
  }
350
687
 
351
688
  // ---- Header management ----
352
689
 
353
690
  setHeader(name, value) {
691
+ validateHeaderName(name);
692
+ validateHeaderValue(name, value);
354
693
  var lower = name.toLowerCase();
355
694
  this._headers[lower] = value;
356
695
  this._headerNames[lower] = name;
@@ -388,33 +727,143 @@ class ClientRequest extends Writable {
388
727
  if (Buffer.isBuffer(chunk)) {
389
728
  chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
390
729
  }
730
+ if (this._sink && this._wholeBody) {
731
+ // end(data) with nothing written before it: the body whole, handed to
732
+ // the transport as its end — sent with a content-length, as node:http
733
+ // sends end(data). Written, then ended, a transport already able to
734
+ // send had begun streaming on the write: no length, and a server that
735
+ // goes by the headers (Express's body-parser) saw no body. It happened
736
+ // whenever the connection was ready at once — a pooled one.
737
+ this._wholeBody = false;
738
+ this._sinkEnded = true;
739
+ this._remember(chunk);
740
+ this._sink.end(chunk, this._trailers);
741
+ callback();
742
+ return;
743
+ }
744
+ this._wholeBody = false;
745
+ if (this._sink) { this._toSink(chunk, callback); return; }
746
+ // No transport yet (still connecting): held — and the writer told to wait
747
+ // past the high-water mark, as a socket not yet connected would.
391
748
  this._bodyChunks.push(chunk);
392
- callback();
749
+ this._bodyBuffered += chunk.byteLength;
750
+ if (this._bodyBuffered < this.writableHighWaterMark) callback();
751
+ else this._pendingWriteCb = callback;
393
752
  }
394
753
 
395
754
  _final(callback) {
396
755
  this.finished = true;
397
- this.emit('_bodyReady');
756
+ if (this._sink && !this._sinkEnded) { this._sinkEnded = true; this._sink.end(null, this._trailers); }
398
757
  callback();
399
758
  }
400
759
 
760
+ // node:http's addTrailers(): fields sent after the body (a HEADERS frame
761
+ // over HTTP/2 and HTTP/3; with chunked encoding over HTTP/1.1). An object,
762
+ // or a list of [name, value] pairs.
763
+ addTrailers(headers) {
764
+ var t = this._trailers || (this._trailers = {});
765
+ var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
766
+ for (var i = 0; i < pairs.length; i++) {
767
+ var name = pairs[i][0], value = pairs[i][1];
768
+ validateHeaderName(name);
769
+ validateHeaderValue(name, value);
770
+ t[String(name).toLowerCase()] = value;
771
+ }
772
+ }
773
+
774
+ // The request's priority (RFC 9218): { urgency: 0-7 (3), incremental
775
+ // (false) } — the `priority` header while the request has not gone, a
776
+ // PRIORITY_UPDATE after it (HTTP/2, HTTP/3), as res.setPriority is the
777
+ // server's. Normalized the same way.
778
+ setPriority(p) {
779
+ this._priority = responsePriority(p);
780
+ if (this._sink && typeof this._sink.setPriority === 'function') this._sink.setPriority(this._priority);
781
+ return this;
782
+ }
783
+
784
+ // ---- The transport that carries the body (client.js) ----
785
+
786
+ // `sink` (a transport's request: write → more?, 'drain', end(chunk,
787
+ // trailers)) takes the request: what is held goes to it, and from now on
788
+ // every write.
789
+ _attachSink(sink) {
790
+ var self = this;
791
+ this._sink = sink;
792
+ if (this._priority && typeof sink.setPriority === 'function') sink.setPriority(this._priority);
793
+ sink.on('drain', function () {
794
+ if (self._sink !== sink || !self._sinkWaitCb) return;
795
+ var cb = self._sinkWaitCb; self._sinkWaitCb = null; cb();
796
+ });
797
+ var held = this._bodyChunks, more = true;
798
+ this._bodyChunks = []; this._bodyBuffered = 0;
799
+ this._sinkEnded = false;
800
+ if (this.finished) {
801
+ // Ended before a transport took it: the body is known whole — one
802
+ // end(), so it goes with its length rather than streamed.
803
+ for (var j = 0; j < held.length; j++) this._remember(held[j]);
804
+ var whole = held.length === 0 ? null : held.length === 1 ? held[0] : Buffer.concat(held);
805
+ this._sinkEnded = true;
806
+ sink.end(whole, this._trailers);
807
+ return;
808
+ }
809
+ for (var i = 0; i < held.length; i++) { this._remember(held[i]); more = sink.write(held[i]); }
810
+ if (this._pendingWriteCb) {
811
+ var cb = this._pendingWriteCb; this._pendingWriteCb = null;
812
+ if (more) cb(); else this._sinkWaitCb = cb;
813
+ }
814
+ }
815
+
816
+ // The transport failed before the server processed the request: the body
817
+ // back here, whole, for the next — or false if it cannot be (more than
818
+ // REPLAY_MAX was handed over: not kept).
819
+ _detachSink() {
820
+ if (!this._replayable) return false;
821
+ this._sink = null;
822
+ this._bodyChunks = this._replay; this._bodyBuffered = this._replayBytes;
823
+ this._replay = []; this._replayBytes = 0;
824
+ if (this._sinkWaitCb) { this._pendingWriteCb = this._sinkWaitCb; this._sinkWaitCb = null; }
825
+ return true;
826
+ }
827
+
828
+ _toSink(chunk, callback) {
829
+ this._remember(chunk);
830
+ if (this._sink.write(chunk)) callback();
831
+ else this._sinkWaitCb = callback;
832
+ }
833
+
834
+ _remember(chunk) {
835
+ if (!this._replayable) return;
836
+ this._replayBytes += chunk.byteLength;
837
+ if (this._replayBytes > REPLAY_MAX) { this._replayable = false; this._replay = []; this._replayBytes = 0; return; }
838
+ this._replay.push(chunk);
839
+ }
840
+
401
841
  // ---- Convenience overrides ----
402
842
 
403
843
  write(chunk, encoding, callback) {
404
844
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
845
+ this._wroteBody = true;
405
846
  return super.write(chunk, encoding, callback);
406
847
  }
407
848
 
408
849
  end(data, encoding, callback) {
409
850
  if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
410
851
  if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
411
- if (data != null) this.write(data, encoding);
852
+ if (data != null) {
853
+ // Nothing written before: this chunk is the body whole (_write).
854
+ this._wholeBody = !this._wroteBody && !this.writableEnded;
855
+ this.write(data, encoding);
856
+ }
412
857
  return super.end(callback);
413
858
  }
414
859
 
860
+ // node:http: 'abort', then 'close' — no error.
415
861
  abort() {
862
+ if (this.aborted || this.destroyed) return;
416
863
  this.aborted = true;
417
- this.destroy(new Error('Request aborted'));
864
+ var self = this;
865
+ process.nextTick(function () { self.emit('abort'); });
866
+ this.destroy();
418
867
  }
419
868
 
420
869
  setTimeout(msecs, callback) {
@@ -426,24 +875,6 @@ class ClientRequest extends Writable {
426
875
  }
427
876
  return this;
428
877
  }
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
878
  }
448
879
 
449
880
 
@@ -452,6 +883,74 @@ class ClientRequest extends Writable {
452
883
  // Must be a real Stream so Node's eos() / Express don't crash.
453
884
  // ============================================================
454
885
 
886
+ /**
887
+ * Make an Express application (or anything built the same way) run on these
888
+ * streams.
889
+ *
890
+ * Express does not use the req and res it is handed as they are: on every
891
+ * request it swaps their prototype for its own (Object.setPrototypeOf(req,
892
+ * app.request)), and that chain ends in node:http's IncomingMessage and
893
+ * ServerResponse. Every method of these classes is gone after that — a
894
+ * request body arrived through _pushData, which no longer existed, and the
895
+ * server fell over on the first POST. The response survived only because its
896
+ * methods are also bound onto each instance (see the constructor), and even
897
+ * so it ran on node:http's getters for its state.
898
+ *
899
+ * So the chain is re-parented instead, once per application:
900
+ *
901
+ * before app.response → express's response methods → http.ServerResponse
902
+ * after app.response → a copy of those methods → quico ServerResponse
903
+ *
904
+ * Express's methods — req.get, res.json, res.send, res.cookie,
905
+ * express.static — sit on top and call the ones below, which are now ours:
906
+ * the same object, on HTTP/1.1, HTTP/2 and HTTP/3.
907
+ *
908
+ * A COPY, because the object holding Express's methods is shared by every
909
+ * Express application in the process: re-parenting it would move them all,
910
+ * including any served by node:http. The copy belongs to this application
911
+ * alone. A sub-application inherits from its parent's objects when it is
912
+ * mounted, so the root is the only one that needs it.
913
+ *
914
+ * Recognised by shape — a function with `request` and `response` objects —
915
+ * not by name; left alone when already adopted or not that shape. Returns
916
+ * the handler, for chaining.
917
+ */
918
+ function adoptApp(handler) {
919
+ if (typeof handler !== 'function') return handler;
920
+ reparent(handler.request, IncomingMessage);
921
+ reparent(handler.response, ServerResponse);
922
+ return handler;
923
+ }
924
+
925
+ // Put `Base` under obj's prototype chain in place of the class it ends in: the
926
+ // objects between obj and the first class prototype (one owning a
927
+ // `constructor`) are copied, the last copy inheriting from Base.prototype.
928
+ function reparent(obj, Base) {
929
+ if (!obj || typeof obj !== 'object' || obj instanceof Base) return;
930
+ var between = [];
931
+ var p = Object.getPrototypeOf(obj);
932
+ while (p && !Object.prototype.hasOwnProperty.call(p, 'constructor')) {
933
+ between.push(p);
934
+ p = Object.getPrototypeOf(p);
935
+ }
936
+ if (!p || p === Object.prototype) return; // not a class chain: not ours to change
937
+ var top = Base.prototype;
938
+ for (var i = between.length - 1; i >= 0; i--) {
939
+ top = Object.create(top, Object.getOwnPropertyDescriptors(between[i]));
940
+ }
941
+ Object.setPrototypeOf(obj, top);
942
+ }
943
+
944
+ // rawHeaders' pairs as node:http's headersDistinct: name → [values].
945
+ function distinctFields(raw) {
946
+ var out = {};
947
+ for (var i = 0; raw && i + 1 < raw.length; i += 2) {
948
+ var n = String(raw[i]).toLowerCase();
949
+ (out[n] || (out[n] = [])).push(String(raw[i + 1]));
950
+ }
951
+ return out;
952
+ }
953
+
455
954
  function _makeSocketStub() {
456
955
  var sock = new Duplex({
457
956
  read(_size) { /* no-op — no data from QUIC socket */ },
@@ -482,4 +981,4 @@ function _makeSocketStub() {
482
981
  }
483
982
 
484
983
 
485
- export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub };
984
+ export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub, adoptApp };