visaljs 0.0.8 → 0.0.9

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.
Files changed (39) hide show
  1. package/README.md +6 -3
  2. package/THIRD_PARTY_LICENSES +1 -1
  3. package/cli.js +55 -4
  4. package/node/prelude/_http_agent.js +5 -0
  5. package/node/prelude/_http_server.js +7 -0
  6. package/node/prelude/dns.js +7 -1
  7. package/node/prelude/events.js +7 -3
  8. package/node/prelude/http.js +685 -279
  9. package/node/prelude/https.js +14 -3
  10. package/node/prelude/internal/events_extras.js +2 -5
  11. package/node/prelude/internal/features.js +17 -0
  12. package/node/prelude/internal/http_agent.js +88 -2
  13. package/node/prelude/internal/http_symbols.js +3 -0
  14. package/node/prelude/internal/net_bound.js +123 -0
  15. package/node/prelude/internal/net_onread.js +73 -0
  16. package/node/prelude/internal/net_tos.js +54 -0
  17. package/node/prelude/internal/perf_observe.js +7 -1
  18. package/node/prelude/internal/tls/wrap.js +2 -1
  19. package/node/prelude/module.js +42 -18
  20. package/node/prelude/net.js +195 -48
  21. package/node/prelude/perf_hooks.js +4 -0
  22. package/node/src/host_http.c +151 -21
  23. package/node/src/host_net.c +75 -9
  24. package/package.json +1 -1
  25. package/prebuilt/win32-x64/boringssl-ce573858472b7fc9.a +0 -0
  26. package/prebuilt/win32-x64/brotli-20d86a61f30baeb3.a +0 -0
  27. package/prebuilt/win32-x64/uv-b11bae65d7fced82.a +0 -0
  28. package/prebuilt/win32-x64/zlib-4ce2317a4c3f5fb5.a +0 -0
  29. package/prebuilt/win32-x64/zstd-015315ac7c784c73.a +0 -0
  30. package/runtime/include/js_atoms.h +4 -0
  31. package/runtime/include/js_web.h +3 -0
  32. package/runtime/include/jscontext.h +8 -0
  33. package/runtime/src/atoms.c +37 -3
  34. package/runtime/src/events.c +19 -0
  35. package/runtime/src/gc.c +4 -4
  36. package/runtime/src/internal.h +4 -4
  37. package/runtime/src/regexp.c +569 -12
  38. package/runtime/src/shape.c +68 -1
  39. package/runtime/src/weak.c +95 -24
@@ -8,13 +8,15 @@
8
8
  // connections close after keepAliveTimeout; server.timeout and setTimeout are the sockets'.
9
9
  // Not yet: the client, headersTimeout and requestTimeout, trailers.
10
10
  import {
11
- httpAttach, httpDate, httpDetach, httpExpired, httpFedClose, httpFedOpen, httpFeed, httpFlush, httpParsers, httpWrite, timerUnref,
11
+ httpAttach, httpBusy, httpDate, httpPending, httpWatchDrain, httpDetach, httpExpired, httpFedClose, httpFedOpen, httpFeed, httpFlush, httpParsers, httpWrite, timerUnref,
12
12
  } from "visaljs:host";
13
13
  import { bindContext, currentContext, runInContext } from "./internal/async_context.js";
14
- import { codeError, invalidArgType, outOfRange, validateInteger, validateString } from "./internal/errors.js";
15
- import { Agent, globalAgent } from "./internal/http_agent.js";
14
+ import { codeError, invalidArgType, invalidArgValue, outOfRange, validateInteger, validateString } from "./internal/errors.js";
15
+ import { Agent, globalAgent, setGlobalAgent } from "./internal/http_agent.js";
16
+ import { kConnectionsCheckingInterval } from "./internal/http_symbols.js";
16
17
  import { createConnection, Server as NetServer } from "./net.js";
17
- import { Readable, Stream } from "./stream.js";
18
+ import { addAbortSignal, Readable, Stream } from "./stream.js";
19
+ import { callableClass } from "./internal/features.js";
18
20
 
19
21
  export const METHODS = ["ACL", "BIND", "CHECKOUT", "CONNECT", "COPY", "DELETE", "GET", "HEAD", "LINK", "LOCK", "M-SEARCH", "MERGE",
20
22
  "MKACTIVITY", "MKCALENDAR", "MKCOL", "MOVE", "NOTIFY", "OPTIONS", "PATCH", "POST", "PROPFIND", "PROPPATCH", "PURGE", "PUT", "QUERY",
@@ -105,12 +107,35 @@ export class IncomingMessage extends Readable {
105
107
 
106
108
  _finish() {
107
109
  this.complete = true;
108
- this.push(null);
110
+ if (!this._readableState.ended) this.push(null);
111
+ }
112
+
113
+ /* Node's: a request without a body (the server's optimizeEmptyRequests) ended and closed at once,
114
+ * without its 'data', 'end' and 'close' */
115
+ _dumpAndCloseReadable() {
116
+ this._dumped = true;
117
+ const state = this._readableState;
118
+ state.ended = true;
119
+ state.endEmitted = true;
120
+ state.destroyed = true;
121
+ state.closed = true;
122
+ state.closeEmitted = true;
123
+ }
124
+
125
+ /* Node's: an AbortSignal that aborts when the connection closes before the response finished */
126
+ get signal() {
127
+ if (!this._abortController) {
128
+ this._abortController = new AbortController();
129
+ if (this.destroyed && (!this.readableEnded || !this.complete)) this._abortController.abort();
130
+ }
131
+ return this._abortController.signal;
109
132
  }
110
133
 
111
- /* the connection closed before the body was complete */
134
+ /* the connection closed before the response was finished (Node's abortIncoming): destroyed, and
135
+ * 'aborted' if its body wasn't all read (_destroy) */
112
136
  _abort() {
113
- if (this.complete || this.destroyed) return;
137
+ this._abortController?.abort();
138
+ if (this.destroyed) return;
114
139
  const e = new Error("aborted");
115
140
  e.code = "ECONNRESET";
116
141
  this.destroy(e);
@@ -143,10 +168,16 @@ const SINGLE = new Set(["age", "authorization", "content-length", "content-type"
143
168
  function addHeader(headers, name, value, join) {
144
169
  const key = name.toLowerCase();
145
170
  const existing = headers[key];
146
- if (existing === undefined) headers[key] = key === "set-cookie" ? [value] : value;
147
- else if (key === "set-cookie") existing.push(value);
148
- else if (SINGLE.has(key) && !join) return;
149
- else headers[key] = existing + (key === "cookie" ? "; " : ", ") + value;
171
+ if (key === "set-cookie") {
172
+ if (existing !== undefined) existing.push(value);
173
+ else headers[key] = [value];
174
+ } else if (SINGLE.has(key) && !join) {
175
+ if (existing === undefined) headers[key] = value; /* duplicates dropped */
176
+ } else if (typeof existing === "string") { /* as Node's: not a value inherited from Object.prototype */
177
+ headers[key] = existing + (key === "cookie" ? "; " : ", ") + value;
178
+ } else {
179
+ headers[key] = value;
180
+ }
150
181
  }
151
182
 
152
183
  /* ---- OutgoingMessage: a response's (and a request's) headers ------------------------------------ */
@@ -168,6 +199,26 @@ export function validateHeaderValue(name, value) {
168
199
 
169
200
  const headersSent = (what) => codeError(Error, "ERR_HTTP_HEADERS_SENT", "Cannot " + what + " headers after they are sent to the client");
170
201
 
202
+ /* Node's write_: a chunk is a string or bytes */
203
+ function validateChunk(chunk) {
204
+ if (chunk === null) throw codeError(TypeError, "ERR_STREAM_NULL_VALUES", "May not write null values to stream");
205
+ if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
206
+ throw invalidArgType("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
207
+ }
208
+ }
209
+
210
+ /* llhttp's error, as Node's: its code and reason, how far it parsed the read, and the read */
211
+ function parseError(code, reason, bytesParsed, rawPacket) {
212
+ const e = new Error("Parse Error: " + reason);
213
+ e.code = code;
214
+ e.reason = reason;
215
+ e.bytesParsed = bytesParsed;
216
+ if (rawPacket !== undefined) {
217
+ e.rawPacket = Buffer.isBuffer(rawPacket) ? rawPacket : Buffer.from(rawPacket.buffer, rawPacket.byteOffset, rawPacket.byteLength);
218
+ }
219
+ return e;
220
+ }
221
+
171
222
  export class OutgoingMessage extends Stream {
172
223
  constructor() {
173
224
  super();
@@ -175,7 +226,7 @@ export class OutgoingMessage extends Stream {
175
226
  this.destroyed = false;
176
227
  this.writableCorked = 0;
177
228
  this.writableNeedDrain = false;
178
- this.writableHighWaterMark = 16384;
229
+ this._highWaterMark = 65536; /* Node's default, until there's a socket */
179
230
  this.headersSent = false;
180
231
  this.finished = false;
181
232
  this.writableEnded = false;
@@ -189,10 +240,18 @@ export class OutgoingMessage extends Stream {
189
240
  this._trailer = "";
190
241
  this._headers = new Map(); /* lowercase name → [name as set, value] */
191
242
  this.socket = null;
243
+ /* Node's: the head once stored, and the bytes kept until there's a socket */
244
+ this.outputData = [];
245
+ this.outputSize = 0;
246
+ this._header = null;
247
+ this._headerSent = false;
248
+ this._last = false;
249
+ this.shouldKeepAlive = true;
250
+ this._closed = false;
192
251
  }
193
252
 
194
253
  setHeader(name, value) {
195
- if (this.headersSent) throw headersSent("set");
254
+ if (this._header || this.headersSent) throw headersSent("set");
196
255
  validateHeaderName(name);
197
256
  validateHeaderValue(name, value);
198
257
  this._headers.set(String(name).toLowerCase(), [String(name), value]);
@@ -200,7 +259,7 @@ export class OutgoingMessage extends Stream {
200
259
  }
201
260
 
202
261
  appendHeader(name, value) {
203
- if (this.headersSent) throw headersSent("append");
262
+ if (this._header || this.headersSent) throw headersSent("append");
204
263
  validateHeaderName(name);
205
264
  validateHeaderValue(name, value);
206
265
  const key = String(name).toLowerCase();
@@ -210,20 +269,94 @@ export class OutgoingMessage extends Stream {
210
269
  return this;
211
270
  }
212
271
 
272
+ /* the socket's, as Node's, else the default */
273
+ get writableHighWaterMark() {
274
+ return this.socket ? this.socket.writableHighWaterMark : this._highWaterMark;
275
+ }
276
+
277
+ set writableHighWaterMark(value) {
278
+ this._highWaterMark = value;
279
+ }
280
+
281
+ /* headers from a Headers or a Map, as Node's: set-cookie's values together */
282
+ setHeaders(headers) {
283
+ if (this._header || this.headersSent) throw headersSent("set");
284
+ if (!headers || Array.isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
285
+ throw invalidArgType("headers", "an instance of Headers or Map", headers);
286
+ }
287
+ let cookies = null;
288
+ for (const [key, value] of headers) {
289
+ if (key === "set-cookie") {
290
+ cookies ??= [];
291
+ if (Array.isArray(value)) cookies.push(...value);
292
+ else cookies.push(value);
293
+ continue;
294
+ }
295
+ this.setHeader(key, value);
296
+ }
297
+ if (cookies !== null) this.setHeader("set-cookie", cookies);
298
+ return this;
299
+ }
300
+
213
301
  get connection() {
214
302
  return this.socket;
215
303
  }
216
304
 
305
+ set connection(value) {
306
+ this.socket = value;
307
+ }
308
+
309
+ /* as Node's: what's kept until there's a socket, and the socket's own */
217
310
  get writableLength() {
218
- return 0;
311
+ return this.outputSize + (this.socket ? this.socket.writableLength : 0);
312
+ }
313
+
314
+ get writableObjectMode() {
315
+ return false;
316
+ }
317
+
318
+ get errored() {
319
+ return this._errored ?? null;
320
+ }
321
+
322
+ get closed() {
323
+ return this._closed;
219
324
  }
220
325
 
221
326
  cork() {
222
327
  this.writableCorked++;
328
+ if (this.socket) this.socket.cork();
223
329
  }
224
330
 
225
331
  uncork() {
226
332
  if (this.writableCorked) this.writableCorked--;
333
+ if (this.socket) this.socket.uncork();
334
+ }
335
+
336
+ /* the socket's timeout, once there is a socket, as Node's */
337
+ setTimeout(msecs, callback) {
338
+ if (callback) this.on("timeout", callback);
339
+ if (!this.socket) this.once("socket", (socket) => socket.setTimeout(msecs));
340
+ else this.socket.setTimeout(msecs);
341
+ return this;
342
+ }
343
+
344
+ /* as Node's: the socket destroyed with it, or 'close' on the next tick without one */
345
+ destroy(error) {
346
+ if (this.destroyed) return this;
347
+ this.destroyed = true;
348
+ this._errored = error;
349
+ if (this.socket) {
350
+ this.socket.destroy(error);
351
+ } else {
352
+ process.nextTick(() => {
353
+ if (!this._closed) {
354
+ this._closed = true;
355
+ this.emit("close");
356
+ }
357
+ });
358
+ }
359
+ return this;
227
360
  }
228
361
 
229
362
  getHeader(name) {
@@ -238,7 +371,7 @@ export class OutgoingMessage extends Stream {
238
371
 
239
372
  removeHeader(name) {
240
373
  validateString(name, "name");
241
- if (this.headersSent) throw headersSent("remove");
374
+ if (this._header || this.headersSent) throw headersSent("remove");
242
375
  if (name.toLowerCase() === "content-length") this._removedContLen = true;
243
376
  this._headers.delete(String(name).toLowerCase());
244
377
  }
@@ -257,6 +390,192 @@ export class OutgoingMessage extends Stream {
257
390
  return [...this._headers.values()].map(([name]) => name);
258
391
  }
259
392
 
393
+ /* Node's OutgoingMessage, as a ClientRequest (and an OutgoingMessage made by hand) writes: the
394
+ * head stored, then the bytes through the socket (ServerResponse has its own, on llhttp's
395
+ * connection). _storeHeader: the first line, the headers as set (or as given in an array), then
396
+ * Connection and the body's framing */
397
+ _storeHeader(firstLine, headersArray) {
398
+ let head = firstLine;
399
+ let hasConnection = false, hasLength = false, hasTE = false, hasTrailer = false;
400
+ const add = (name, value) => {
401
+ const key = String(name).toLowerCase();
402
+ if (key === "connection") {
403
+ hasConnection = true;
404
+ if (/close/i.test(String(value))) this._last = true;
405
+ else this.shouldKeepAlive = true;
406
+ } else if (key === "content-length") hasLength = true;
407
+ else if (key === "transfer-encoding") {
408
+ hasTE = true;
409
+ if (/chunked/i.test(String(value))) this.chunkedEncoding = true;
410
+ } else if (key === "trailer") hasTrailer = true;
411
+ if (Array.isArray(value)) for (const v of value) head += name + ": " + v + "\r\n";
412
+ else head += name + ": " + value + "\r\n";
413
+ };
414
+ if (headersArray) {
415
+ if (Array.isArray(headersArray[0])) for (const [name, value] of headersArray) add(name, value);
416
+ else for (let i = 0; i < headersArray.length; i += 2) add(headersArray[i], headersArray[i + 1]);
417
+ } else {
418
+ for (const [, [name, value]] of this._headers) add(name, value);
419
+ }
420
+ if (!hasConnection) {
421
+ const shouldSendKeepAlive = this.shouldKeepAlive && (hasLength || this.useChunkedEncodingByDefault || this.agent);
422
+ if (shouldSendKeepAlive) head += "Connection: keep-alive\r\n";
423
+ else {
424
+ this._last = true;
425
+ head += "Connection: close\r\n";
426
+ }
427
+ }
428
+ if (!hasLength && !hasTE) {
429
+ if (!this._hasBody) this.chunkedEncoding = false;
430
+ else if (!this.useChunkedEncodingByDefault) this._last = true;
431
+ else if (!hasTrailer && !this._removedContLen && typeof this._contentLength === "number") head += "Content-Length: " + this._contentLength + "\r\n";
432
+ else {
433
+ head += "Transfer-Encoding: chunked\r\n";
434
+ this.chunkedEncoding = true;
435
+ }
436
+ }
437
+ this._header = head + "\r\n";
438
+ this._headerSent = false;
439
+ this.headersSent = true;
440
+ }
441
+
442
+ /* bytes for the socket: written now, or kept until it's assigned */
443
+ _writeRaw(data, encoding, callback) {
444
+ const conn = this.socket;
445
+ if (conn?.destroyed) return false;
446
+ if (conn && conn._httpMessage === this && conn.writable) {
447
+ if (this.outputData.length) this._flushOutput(conn);
448
+ return conn.write(data, encoding, callback);
449
+ }
450
+ this.outputData.push({ data, encoding, callback });
451
+ this.outputSize += data.length;
452
+ return this.outputSize < this.writableHighWaterMark;
453
+ }
454
+
455
+ /* the head goes with the first bytes: in the same string when it can be */
456
+ _send(data, encoding, callback) {
457
+ if (!this._headerSent && this._header !== null) {
458
+ this._headerSent = true;
459
+ if (typeof data === "string" && (encoding === "utf8" || encoding === "latin1" || !encoding)) data = this._header + data;
460
+ else this._writeRaw(this._header, "latin1", null);
461
+ }
462
+ return this._writeRaw(data, encoding, callback);
463
+ }
464
+
465
+ _flushOutput(socket) {
466
+ const output = this.outputData;
467
+ if (output.length === 0) return undefined;
468
+ this.outputData = [];
469
+ this.outputSize = 0;
470
+ let ret;
471
+ for (const { data, encoding, callback } of output) ret = socket.write(data, encoding, callback);
472
+ return ret;
473
+ }
474
+
475
+ _flush() {
476
+ const socket = this.socket;
477
+ if (socket?.writable) {
478
+ const ret = this._flushOutput(socket);
479
+ if (!this.finished && ret && this._needDrain) {
480
+ this._needDrain = false;
481
+ this.emit("drain");
482
+ }
483
+ }
484
+ }
485
+
486
+ write(chunk, encoding, callback) {
487
+ if (typeof encoding === "function") {
488
+ callback = encoding;
489
+ encoding = null;
490
+ }
491
+ validateChunk(chunk); /* first, as Node's write_ (called on another object too) */
492
+ const ret = this._write_(chunk, encoding, callback, false);
493
+ if (!ret) this._needDrain = true;
494
+ return ret;
495
+ }
496
+
497
+ _write_(chunk, encoding, callback, fromEnd) {
498
+ if (typeof callback !== "function") callback = () => {};
499
+ if (chunk === null) throw codeError(TypeError, "ERR_STREAM_NULL_VALUES", "May not write null values to stream");
500
+ if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
501
+ throw invalidArgType("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
502
+ }
503
+ let err;
504
+ if (this.finished) err = codeError(Error, "ERR_STREAM_WRITE_AFTER_END", "write after end");
505
+ else if (this.destroyed) err = codeError(Error, "ERR_STREAM_DESTROYED", "Cannot call write after a stream was destroyed");
506
+ if (err) {
507
+ if (!this.destroyed) process.nextTick(() => this.emit("error", err));
508
+ process.nextTick(callback, err);
509
+ return false;
510
+ }
511
+ const len = typeof chunk === "string" ? Buffer.byteLength(chunk, encoding || "utf8") : chunk.byteLength;
512
+ if (!this._header) {
513
+ if (fromEnd) this._contentLength = len;
514
+ this._implicitHeader();
515
+ }
516
+ if (!this._hasBody) {
517
+ process.nextTick(callback);
518
+ return true;
519
+ }
520
+ if (this.chunkedEncoding && len !== 0) {
521
+ this._send(len.toString(16), "latin1", null);
522
+ this._send("\r\n", "latin1", null);
523
+ this._send(chunk, encoding, null);
524
+ return this._send("\r\n", "latin1", callback);
525
+ }
526
+ return this._send(chunk, encoding, callback);
527
+ }
528
+
529
+ end(chunk, encoding, callback) {
530
+ if (typeof chunk === "function") {
531
+ callback = chunk;
532
+ chunk = null;
533
+ encoding = null;
534
+ } else if (typeof encoding === "function") {
535
+ callback = encoding;
536
+ encoding = null;
537
+ }
538
+ if (chunk) {
539
+ if (this.finished) {
540
+ this._write_(chunk, encoding, null, true);
541
+ return this;
542
+ }
543
+ this._write_(chunk, encoding, null, true);
544
+ } else if (this.finished) {
545
+ if (typeof callback === "function") {
546
+ if (!this.writableFinished) this.on("finish", callback);
547
+ else callback(codeError(Error, "ERR_STREAM_ALREADY_FINISHED", "Cannot call end after a stream was finished"));
548
+ }
549
+ return this;
550
+ } else if (!this._header) {
551
+ this._contentLength = 0;
552
+ this._implicitHeader();
553
+ }
554
+ if (typeof callback === "function") this.once("finish", callback);
555
+ const finish = () => {
556
+ if (this.socket && this.socket._hadError) return;
557
+ this.writableFinished = true;
558
+ this.emit("finish");
559
+ };
560
+ if (this._hasBody && this.chunkedEncoding) this._send("0\r\n" + this._trailer + "\r\n", "latin1", finish);
561
+ else if (!this._headerSent || chunk) this._send("", "latin1", finish);
562
+ else process.nextTick(finish);
563
+ this.finished = true;
564
+ this.writableEnded = true;
565
+ this.writable = false;
566
+ return this;
567
+ }
568
+
569
+ flushHeaders() {
570
+ if (!this._header) this._implicitHeader();
571
+ this._send("");
572
+ }
573
+
574
+ /* as Node's: a subclass renders its first line (ClientRequest, ServerResponse) */
575
+ _implicitHeader() {
576
+ throw codeError(Error, "ERR_METHOD_NOT_IMPLEMENTED", "The _implicitHeader() method is not implemented");
577
+ }
578
+
260
579
  /* trailers, sent after a chunked body: { name: value } or [[name, value], …] */
261
580
  addTrailers(headers) {
262
581
  this._trailer = "";
@@ -286,11 +605,37 @@ export class ServerResponse extends OutgoingMessage {
286
605
  this.statusMessage = undefined;
287
606
  this.sendDate = true;
288
607
  this._conn = conn;
289
- this._keepAlive = keepAlive;
290
- this._keepAliveTimeout = server ? server.keepAliveTimeout : 5000;
608
+ /* one made by hand (new ServerResponse(req)) is kept alive, without a Keep-Alive timeout, as Node's */
609
+ this._keepAlive = keepAlive ?? true;
610
+ this._keepAliveTimeout = server ? server.keepAliveTimeout : 0;
291
611
  this._head = req.method === "HEAD";
292
612
  }
293
613
 
614
+ /* the response's bytes: to the connection's parser, which corks them; or, for a response made by
615
+ * hand (new ServerResponse(req), Node's assignSocket), to its socket, kept until it has one */
616
+ _send(data, latin1 = false) {
617
+ if (this._queued) (this._unsent ??= []).push([data, latin1]); /* behind the connection's earlier responses */
618
+ else if (this._conn !== undefined) httpWrite(this._conn, data, latin1);
619
+ else if (this.socket) this.socket.write(data, latin1 ? "latin1" : undefined);
620
+ else (this._unsent ??= []).push([data, latin1]);
621
+ }
622
+
623
+ /** A socket for a response made by hand, as Node's: its writes go there. */
624
+ assignSocket(socket) {
625
+ if (socket._httpMessage) throw codeError(Error, "ERR_HTTP_SOCKET_ASSIGNED", "Socket already assigned");
626
+ socket._httpMessage = this;
627
+ this.socket = socket;
628
+ this.emit("socket", socket);
629
+ const unsent = this._unsent;
630
+ this._unsent = undefined;
631
+ if (unsent) for (const [data, latin1] of unsent) socket.write(data, latin1 ? "latin1" : undefined);
632
+ }
633
+
634
+ detachSocket(socket) {
635
+ socket._httpMessage = null;
636
+ this.socket = null;
637
+ }
638
+
294
639
  setTimeout(msecs, callback) {
295
640
  if (typeof callback === "function") this.on("timeout", callback);
296
641
  if (this.socket) this.socket.setTimeout(msecs);
@@ -313,12 +658,13 @@ export class ServerResponse extends OutgoingMessage {
313
658
  flushHeaders() {
314
659
  if (!this.headersSent && Object.hasOwn(this, "writeHead")) this._implicitHeader(undefined); /* a wrapper sees it */
315
660
  const head = this._takeHead(undefined);
316
- if (head) httpWrite(this._conn, head);
661
+ if (head) this._send(head, true);
317
662
  }
318
663
 
319
664
  destroy(error) {
320
665
  if (this.destroyed) return this;
321
666
  this.destroyed = true;
667
+ this._errored = error;
322
668
  this.writable = false;
323
669
  this._closeEmitted = true;
324
670
  if (this.socket) this.socket.destroy();
@@ -339,6 +685,13 @@ export class ServerResponse extends OutgoingMessage {
339
685
  }
340
686
 
341
687
  writeHead(statusCode, statusMessage, headers) {
688
+ /* as Node's: an integer status from 100 to 999 */
689
+ const originalStatusCode = statusCode;
690
+ statusCode |= 0;
691
+ if (statusCode < 100 || statusCode > 999) {
692
+ const shown = typeof originalStatusCode === "object" && originalStatusCode !== null ? JSON.stringify(originalStatusCode) : String(originalStatusCode);
693
+ throw codeError(RangeError, "ERR_HTTP_INVALID_STATUS_CODE", `Invalid status code: ${shown}`);
694
+ }
342
695
  if (typeof statusMessage !== "string") {
343
696
  headers = statusMessage;
344
697
  statusMessage = undefined;
@@ -346,7 +699,10 @@ export class ServerResponse extends OutgoingMessage {
346
699
  this.statusCode = statusCode;
347
700
  if (statusMessage !== undefined) this.statusMessage = statusMessage;
348
701
  if (Array.isArray(headers)) {
349
- for (let i = 0; i < headers.length; i += 2) this.appendHeader(headers[i], headers[i + 1]);
702
+ /* as Node's: they replace the headers set before, and may repeat a name among themselves */
703
+ if (headers.length % 2 !== 0) throw invalidArgValue("headers", "is invalid", headers);
704
+ for (let i = 0; i < headers.length; i += 2) if (this.hasHeader(headers[i])) this.removeHeader(headers[i]);
705
+ for (let i = 0; i < headers.length; i += 2) if (headers[i]) this.appendHeader(headers[i], headers[i + 1]);
350
706
  } else if (headers) {
351
707
  for (const name of Object.keys(headers)) this.setHeader(name, headers[name]);
352
708
  }
@@ -398,36 +754,82 @@ export class ServerResponse extends OutgoingMessage {
398
754
  this.chunkedEncoding = true;
399
755
  }
400
756
  }
757
+ /* as Node's _storeHeader: only a chunked body can have trailers (the head isn't sent) */
758
+ if (this.chunkedEncoding !== true && this._headers.has("trailer")) {
759
+ throw codeError(Error, "ERR_HTTP_TRAILER_INVALID", "Trailers are invalid with this transfer encoding");
760
+ }
401
761
  this.headersSent = true;
402
762
  return head + "\r\n";
403
763
  }
404
764
 
405
765
  write(chunk, encoding, callback) {
406
766
  if (typeof encoding === "function") callback = encoding;
767
+ validateChunk(chunk);
407
768
  if (this.writableEnded || this.destroyed) {
408
- const e = new Error(this.destroyed ? "Cannot call write after a stream was destroyed" : "write after end");
409
- e.code = this.destroyed ? "ERR_STREAM_DESTROYED" : "ERR_STREAM_WRITE_AFTER_END";
410
- /* Node's emitErrorNt: none on a destroyed response (its client went away) */
411
- if (!this.destroyed) queueMicrotask(() => this.emit("error", e));
412
- if (typeof callback === "function") queueMicrotask(() => callback(e));
769
+ /* Node's write_: after end before destroyed; the callback, then 'error' (Node's emitErrorNt),
770
+ * none on a destroyed response (it closed, or its client went away) */
771
+ const ended = this.writableEnded;
772
+ const e = new Error(ended ? "write after end" : "Cannot call write after a stream was destroyed");
773
+ e.code = ended ? "ERR_STREAM_WRITE_AFTER_END" : "ERR_STREAM_DESTROYED";
774
+ if (!this.destroyed) {
775
+ process.nextTick(() => {
776
+ if (typeof callback === "function") callback(e);
777
+ if (!this.destroyed) this.emit("error", e);
778
+ });
779
+ } else if (typeof callback === "function") {
780
+ process.nextTick(callback, e);
781
+ }
413
782
  return false;
414
783
  }
415
784
  if (typeof chunk === "string" && typeof encoding === "string" && encoding !== "utf8" && encoding !== "utf-8") chunk = Buffer.from(chunk, encoding);
416
785
  /* a writeHead of the response's own (a wrapper) sees the implicit head, as in Node */
417
786
  if (!this.headersSent && Object.hasOwn(this, "writeHead")) this._implicitHeader(undefined);
418
787
  const head = this._takeHead(undefined);
419
- if (head) httpWrite(this._conn, head);
788
+ if (head) this._send(head, true);
420
789
  if (chunk !== undefined && chunk !== null && chunkLength(chunk) > 0 && !this._head) {
421
790
  if (this.chunkedEncoding) {
422
- httpWrite(this._conn, chunkLength(chunk).toString(16) + "\r\n");
423
- httpWrite(this._conn, chunk);
424
- httpWrite(this._conn, "\r\n");
791
+ this._send(chunkLength(chunk).toString(16) + "\r\n");
792
+ this._send(chunk);
793
+ this._send("\r\n");
425
794
  } else {
426
- httpWrite(this._conn, chunk);
795
+ this._send(chunk);
427
796
  }
428
797
  }
429
798
  if (typeof callback === "function") queueMicrotask(callback);
430
- return true;
799
+ return this._underHighWaterMark();
800
+ }
801
+
802
+ get writableLength() {
803
+ return this._pendingBytes();
804
+ }
805
+
806
+ /* bytes written that haven't gone out: corked in C and in libuv's queue, or a TLS socket's */
807
+ _pendingBytes() {
808
+ if (this._conn === undefined || this._queued) return 0;
809
+ return isFed(this.socket) ? httpPending(this._conn) + (this.socket?.writableLength ?? 0) : httpPending(this._conn);
810
+ }
811
+
812
+ /* write()'s answer, as Node's: false over the high-water mark, with 'drain' once it all went out */
813
+ _underHighWaterMark() {
814
+ if (this._pendingBytes() < this.writableHighWaterMark) return true;
815
+ /* over it while corked: sent now, libuv writing what the kernel takes at once, as Node's socket
816
+ * does; only what's left counts */
817
+ if (!isFed(this.socket)) {
818
+ httpFlush(this._conn);
819
+ if (this._pendingBytes() < this.writableHighWaterMark) return true;
820
+ }
821
+ if (!this.writableNeedDrain) {
822
+ this.writableNeedDrain = true;
823
+ if (isFed(this.socket)) this.socket.once("drain", () => this._emitDrain());
824
+ else if (httpWatchDrain(this._conn)) process.nextTick(() => this._emitDrain());
825
+ }
826
+ return false;
827
+ }
828
+
829
+ _emitDrain() {
830
+ if (!this.writableNeedDrain) return;
831
+ this.writableNeedDrain = false;
832
+ this.emit("drain");
431
833
  }
432
834
 
433
835
  end(chunk, encoding, callback) {
@@ -437,34 +839,66 @@ export class ServerResponse extends OutgoingMessage {
437
839
  } else if (typeof encoding === "function") {
438
840
  callback = encoding;
439
841
  }
842
+ if (chunk !== undefined && chunk !== null) validateChunk(chunk);
440
843
  if (this.writableEnded) return this;
441
844
  if (typeof chunk === "string" && typeof encoding === "string" && encoding !== "utf8" && encoding !== "utf-8") chunk = Buffer.from(chunk, encoding);
442
845
  if (!this.headersSent && Object.hasOwn(this, "writeHead")) this._implicitHeader(chunk !== undefined && chunk !== null ? chunkLength(chunk) : 0);
443
846
  if (!this.headersSent) {
444
847
  const hasChunk = chunk !== undefined && chunk !== null;
445
848
  const length = hasChunk ? chunkLength(chunk) : 0;
446
- if (hasChunk && typeof chunk === "string" && !this._head) httpWrite(this._conn, this._renderHead(length) + chunk);
447
- else {
448
- httpWrite(this._conn, this._renderHead(length));
449
- if (hasChunk && length && !this._head) httpWrite(this._conn, chunk);
849
+ if (hasChunk && typeof chunk === "string" && !this._head) {
850
+ /* the head's bytes are latin1, the body's UTF-8: corked into one write by the parser's
851
+ * connection; a response made by hand writes them as one string, as Node's _send */
852
+ if (this._conn === undefined) {
853
+ this._send(this._renderHead(length) + chunk);
854
+ } else {
855
+ this._send(this._renderHead(length), true);
856
+ this._send(chunk);
857
+ }
858
+ } else {
859
+ this._send(this._renderHead(length), true);
860
+ if (hasChunk && length && !this._head) this._send(chunk);
450
861
  }
451
862
  } else {
452
863
  if (chunk !== undefined && chunk !== null) this.write(chunk);
453
864
  const head = this._takeHead(undefined);
454
- if (head) httpWrite(this._conn, head);
455
- if (this.chunkedEncoding && !this._head) httpWrite(this._conn, "0\r\n" + this._trailer + "\r\n");
865
+ if (head) this._send(head, true);
866
+ if (this.chunkedEncoding && !this._head) this._send("0\r\n" + this._trailer + "\r\n", true);
456
867
  }
457
868
  this.writableEnded = this.finished = true;
458
869
  this.writable = false;
459
- if (!this._keepAlive && this.socket) { /* the response goes out, then the socket ends */
460
- httpFlush(this._conn);
461
- this.socket.destroySoon();
870
+ if (this._queued) { /* finished once it has gone out, after the responses before it */
871
+ this._endCallback = callback;
872
+ return this;
873
+ }
874
+ this._afterEnd(callback);
875
+ return this;
876
+ }
877
+
878
+ /* a response that waited behind the connection's earlier ones: its turn (Node's assignSocket) */
879
+ _activate() {
880
+ this._queued = false;
881
+ const unsent = this._unsent;
882
+ this._unsent = undefined;
883
+ if (unsent) for (const [data, latin1] of unsent) httpWrite(this._conn, data, latin1);
884
+ if (this.writableEnded) this._afterEnd(this._endCallback);
885
+ }
886
+
887
+ /* the response is all written: the socket ends after the last one (not kept alive, or the client
888
+ * ended its side), then 'finish' and 'close' */
889
+ _afterEnd(callback) {
890
+ /* the response is all written: sent now, so that by 'finish' it has gone to the socket, as Node's */
891
+ if (this._conn !== undefined) httpFlush(this._conn);
892
+ if ((!this._keepAlive || this._last) && this.socket) { /* the response goes out, then the socket ends */
893
+ if (typeof this.socket.destroySoon === "function") this.socket.destroySoon(); /* as Node's resOnFinish */
894
+ else this.socket.end();
462
895
  }
463
896
  queueMicrotask(() => {
464
897
  this.writableFinished = true;
465
898
  this.emit("finish");
466
899
  if (!this._closeEmitted) {
467
900
  this._closeEmitted = true;
901
+ this.destroyed = true; /* as Node's emitCloseNT */
468
902
  this.emit("close");
469
903
  }
470
904
  if (typeof callback === "function") callback();
@@ -485,11 +919,11 @@ function noop() {}
485
919
  function serverArguments(options, requestListener) {
486
920
  if (typeof options === "function") return [{}, options];
487
921
  if (options === undefined || options === null) return [{}, requestListener];
488
- if (typeof options !== "object") throw invalidArgType("options", "of type object", options);
922
+ if (typeof options !== "object" || Array.isArray(options)) throw invalidArgType("options", "of type object", options); /* validateObject */
489
923
  return [options, requestListener];
490
924
  }
491
925
 
492
- export class Server extends NetServer {
926
+ class ServerClass extends NetServer {
493
927
  constructor(options, requestListener) {
494
928
  [options, requestListener] = serverArguments(options, requestListener);
495
929
  super({ allowHalfOpen: true, noDelay: options.noDelay ?? true, keepAlive: options.keepAlive, keepAliveInitialDelay: options.keepAliveInitialDelay });
@@ -521,6 +955,10 @@ export class Server extends NetServer {
521
955
  this.insecureHTTPParser = boolean("insecureHTTPParser", undefined);
522
956
  this.requireHostHeader = boolean("requireHostHeader", true);
523
957
  this.joinDuplicateHeaders = boolean("joinDuplicateHeaders", undefined);
958
+ /* the classes requests and responses are made of (subclasses of node:http's), as Node's */
959
+ this._IncomingMessage = options.IncomingMessage || IncomingMessage;
960
+ this._ServerResponse = options.ServerResponse || ServerResponse;
961
+ this._optimizeEmptyRequests = boolean("optimizeEmptyRequests", false);
524
962
  this.requestTimeout = integer("requestTimeout", 300_000);
525
963
  this.headersTimeout = integer("headersTimeout", Math.min(60_000, this.requestTimeout));
526
964
  if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) {
@@ -528,27 +966,63 @@ export class Server extends NetServer {
528
966
  }
529
967
  this.keepAliveTimeout = integer("keepAliveTimeout", 5000);
530
968
  this.connectionsCheckingInterval = integer("connectionsCheckingInterval", 30_000);
531
- this._checking = null; /* the connections-checking timer, unref'd, while listening */
532
969
  this._parsers = -1;
533
970
  this._states = new Map(); /* connection → { socket, req, res, keepAliveTimeoutSet } */
534
971
  this.on(connectionEvent, (socket) => this._onSocket(socket));
535
- this.on("listening", () => this._checkConnectionsLater());
972
+ this.on("listening", () => this._startConnectionsChecking());
973
+ }
974
+
975
+ /* captureRejections: a 'request' listener's rejection answers 500, its headers dropped (or the
976
+ * response destroyed, once sent), as Node's; another event's goes to net.Server's */
977
+ [Symbol.for("nodejs.rejection")](err, event, ...args) {
978
+ if (event === "request") {
979
+ const res = args[1];
980
+ if (!res.headersSent && !res.writableEnded) {
981
+ for (const name of res.getHeaderNames()) res.removeHeader(name);
982
+ res.statusCode = 500;
983
+ res.end(STATUS_CODES[500]);
984
+ } else {
985
+ res.destroy();
986
+ }
987
+ return;
988
+ }
989
+ const base = NetServer.prototype[Symbol.for("nodejs.rejection")];
990
+ if (typeof base === "function") base.call(this, err, event, ...args);
991
+ else this.emit("error", err);
992
+ }
993
+
994
+ /* Node's setupConnectionsTracking: the connections checked every connectionsCheckingInterval ms
995
+ * (unref'd), a new timer for each 'listening'. server[kConnectionsCheckingInterval] stands for
996
+ * Node's interval: its _destroyed says whether it was stopped. */
997
+ _startConnectionsChecking() {
998
+ this._stopConnectionsChecking();
999
+ const interval = { _destroyed: false, _timer: null };
1000
+ this[kConnectionsCheckingInterval] = interval;
1001
+ const tick = () => {
1002
+ if (interval._destroyed) return;
1003
+ this._checkConnections();
1004
+ interval._timer = timerUnref(tick, this.connectionsCheckingInterval);
1005
+ };
1006
+ if (this.connectionsCheckingInterval > 0) interval._timer = timerUnref(tick, this.connectionsCheckingInterval);
1007
+ }
1008
+
1009
+ _stopConnectionsChecking() {
1010
+ const interval = this[kConnectionsCheckingInterval];
1011
+ if (!interval || interval._destroyed) return;
1012
+ interval._destroyed = true;
1013
+ if (interval._timer !== null) clearTimeout(interval._timer);
1014
+ interval._timer = null;
536
1015
  }
537
1016
 
538
1017
  /* Node's checkConnections: a request (or its head) over requestTimeout (headersTimeout) gets a 408 */
539
- _checkConnectionsLater() {
540
- if (this.connectionsCheckingInterval <= 0 || this._checking !== null) return;
541
- this._checking = timerUnref(() => {
542
- this._checking = null;
543
- if (this._handle === null) return;
544
- if (this._parsers !== -1 && (this.headersTimeout > 0 || this.requestTimeout > 0)) {
545
- for (const conn of httpExpired(this._parsers, this.headersTimeout, this.requestTimeout)) {
546
- const state = this._states.get(conn);
547
- if (state) socketOnError.call(state.socket, codeError(Error, "ERR_HTTP_REQUEST_TIMEOUT", "Request timeout"));
548
- }
1018
+ _checkConnections() {
1019
+ if (this._handle === null) return;
1020
+ if (this._parsers !== -1 && (this.headersTimeout > 0 || this.requestTimeout > 0)) {
1021
+ for (const conn of httpExpired(this._parsers, this.headersTimeout, this.requestTimeout)) {
1022
+ const state = this._states.get(conn);
1023
+ if (state) socketOnError.call(state.socket, codeError(Error, "ERR_HTTP_REQUEST_TIMEOUT", "Request timeout"));
549
1024
  }
550
- this._checkConnectionsLater();
551
- }, this.connectionsCheckingInterval);
1025
+ }
552
1026
  }
553
1027
 
554
1028
  setTimeout(msecs, callback) {
@@ -563,13 +1037,15 @@ export class Server extends NetServer {
563
1037
  }
564
1038
 
565
1039
  closeIdleConnections() {
566
- for (const state of [...this._states.values()]) if (!state.req && !state.res) state.socket.destroy();
1040
+ /* as Node's: idle is no request under way (none begun on it, nor a response unfinished) */
1041
+ for (const state of [...this._states.values()]) {
1042
+ if (state.incoming.length === 0 && !state.res && !httpBusy(state.conn)) state.socket.destroy();
1043
+ }
567
1044
  }
568
1045
 
569
1046
  close(callback) {
570
1047
  this.closeIdleConnections();
571
- if (this._checking !== null) clearTimeout(this._checking);
572
- this._checking = null;
1048
+ this._stopConnectionsChecking();
573
1049
  return super.close(callback);
574
1050
  }
575
1051
 
@@ -579,12 +1055,17 @@ export class Server extends NetServer {
579
1055
  /* in the async context of the connection's, as the server's sockets' callbacks */
580
1056
  this._parsers = httpParsers(...[
581
1057
  (conn, method, url, raw, major, minor, keepAlive, upgrade) => this._onRequest(conn, method, url, raw, major, minor, keepAlive, upgrade),
582
- (conn, chunk) => this._states.get(conn)?.req?._push(chunk),
1058
+ (conn, chunk) => {
1059
+ /* the bytes llhttp reads in C are the socket's activity, as Node's onParserExecute counts them */
1060
+ const state = this._states.get(conn);
1061
+ state?.socket._unrefTimer?.();
1062
+ state?.req?._push(chunk);
1063
+ },
583
1064
  (conn, trailers) => {
584
1065
  const state = this._states.get(conn);
585
1066
  const req = state?.req;
586
1067
  if (req) {
587
- if (state.res === null || state.res.writableEnded) state.req = null;
1068
+ state.req = null; /* parsed; it stays in state.incoming until its response finishes */
588
1069
  if (trailers) {
589
1070
  req.rawTrailers = trailers;
590
1071
  for (let i = 0; i < trailers.length; i += 2) addHeader(req.trailers, trailers[i], trailers[i + 1]);
@@ -593,42 +1074,64 @@ export class Server extends NetServer {
593
1074
  }
594
1075
  },
595
1076
  (conn, head) => this._onUpgrade(conn, head),
596
- (conn, code, reason) => {
1077
+ (conn, code, reason, bytesParsed, rawPacket) => {
597
1078
  const state = this._states.get(conn);
598
1079
  if (!state) return;
599
- const e = new Error("Parse Error: " + reason);
600
- e.code = code;
601
- e.reason = reason;
1080
+ const e = parseError(code, reason, bytesParsed, rawPacket);
602
1081
  socketOnError.call(state.socket, e);
603
1082
  },
1083
+ /* the connection's output went out: the response waiting for it gets 'drain' */
1084
+ (conn) => this._states.get(conn)?.res?._emitDrain(),
604
1085
  ].map(bindContext));
605
1086
  }
606
1087
  const conn = connOf(socket);
607
1088
  if (conn === null) return;
608
- const state = { socket, conn, req: null, res: null, keepAliveTimeoutSet: false, listeners: null };
1089
+ socket.server = this; /* as Node's: the http server, even for a socket another server accepted */
1090
+ /* as Node's connection state: the request being parsed (req); those whose responses aren't
1091
+ * finished (incoming); the response writing to the connection (res), and those waiting behind it
1092
+ * (outgoing: pipelined requests are answered in order) */
1093
+ const state = { socket, conn, req: null, incoming: [], res: null, outgoing: [], keepAliveTimeoutSet: false, listeners: null };
609
1094
  this._states.set(conn, state);
610
1095
  if (this.timeout && typeof socket.setTimeout === "function") socket.setTimeout(this.timeout);
611
1096
  const onTimeout = () => this._onTimeout(state);
1097
+ /* Node's socketOnEnd: the client ended its side; with httpAllowHalfOpen, the socket ends after
1098
+ * the last response, else now */
612
1099
  const onEnd = () => {
613
- this._abortIncoming(state);
614
- if (socket.writable) socket.end();
1100
+ if (!this.httpAllowHalfOpen) {
1101
+ if (socket.writable) socket.end();
1102
+ } else if (state.outgoing.length) {
1103
+ state.outgoing[state.outgoing.length - 1]._last = true;
1104
+ } else if (state.res) {
1105
+ state.res._last = true;
1106
+ } else if (socket.writable) {
1107
+ socket.end();
1108
+ }
615
1109
  };
1110
+ /* Node's socketOnClose: the requests and responses not finished are aborted */
616
1111
  const onClose = () => {
617
1112
  this._states.delete(conn);
618
1113
  this._abortIncoming(state);
619
- const res = state.res;
1114
+ const responses = state.res ? [state.res, ...state.outgoing] : state.outgoing;
620
1115
  state.res = null;
621
- if (res) res._connectionClosed();
1116
+ state.outgoing = [];
1117
+ for (const res of responses) res._connectionClosed();
622
1118
  };
623
1119
  state.listeners = { timeout: onTimeout, end: onEnd, close: onClose, error: socketOnError };
624
1120
  for (const [event, fn] of Object.entries(state.listeners)) socket.on(event, fn);
625
1121
  httpAttach(conn, this._parsers, false, this.maxHeaderSize ?? maxHeaderSize);
1122
+ /* bytes the socket read before it was the server's (a program that peeked, then emitted
1123
+ * 'connection' itself): parsed first, a chunk at a time, as Node's 'data' brings them */
1124
+ const readable = socket._readableState;
1125
+ while (readable && !isFed(socket) && socket.readableLength > 0 && this._states.has(conn)) {
1126
+ const chunk = socket.read(readable.buffer[readable.bufferIndex]?.length ?? socket.readableLength);
1127
+ if (chunk === null) break;
1128
+ httpFeed(conn, typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1129
+ }
626
1130
  }
627
1131
 
628
1132
  _abortIncoming(state) {
629
- const req = state.req;
630
1133
  state.req = null;
631
- if (req) req._abort();
1134
+ for (const req of state.incoming.splice(0)) req._abort();
632
1135
  }
633
1136
 
634
1137
  /* Node's socketOnTimeout: the request's, the response's or the server's 'timeout', or the end */
@@ -645,11 +1148,12 @@ export class Server extends NetServer {
645
1148
  const state = this._states.get(conn);
646
1149
  if (!state) return 0;
647
1150
  const socket = state.socket;
1151
+ socket._unrefTimer?.(); /* the head's bytes are the socket's activity */
648
1152
  if (state.keepAliveTimeoutSet) {
649
1153
  socket.setTimeout(0);
650
1154
  state.keepAliveTimeoutSet = false;
651
1155
  }
652
- const req = new IncomingMessage(socket);
1156
+ const req = new this._IncomingMessage(socket);
653
1157
  req.method = method;
654
1158
  req.url = url;
655
1159
  req.httpVersionMajor = major;
@@ -663,25 +1167,43 @@ export class Server extends NetServer {
663
1167
  return 2;
664
1168
  }
665
1169
  state.req = req;
666
- const res = new ServerResponse(req, conn, keepAlive, this);
667
- state.res = res;
1170
+ state.incoming.push(req);
1171
+ if (this._optimizeEmptyRequests && !("content-length" in req.headers) && !("transfer-encoding" in req.headers)) req._dumpAndCloseReadable();
1172
+ const res = new this._ServerResponse(req, conn, keepAlive, this);
1173
+ if (state.res) { /* a pipelined request: its response waits for the one before */
1174
+ res._queued = true;
1175
+ state.outgoing.push(res);
1176
+ } else {
1177
+ state.res = res;
1178
+ }
1179
+ res.once("finish", () => this._resOnFinish(state, req, res));
668
1180
  if (this.requireHostHeader && major === 1 && minor === 1 && !req.headers.host) { /* Node: a 400, and the connection ends */
669
1181
  res.writeHead(400, ["Connection", "close"]);
670
1182
  res.end();
671
1183
  return 0;
672
1184
  }
673
- res.once("finish", () => {
674
- if (state.res === res) state.res = null;
675
- if (state.req === req && req.complete) state.req = null;
676
- if (res._keepAlive && !state.res && this.keepAliveTimeout && socket.writable) {
677
- socket.setTimeout(this.keepAliveTimeout);
678
- state.keepAliveTimeoutSet = true;
679
- }
680
- });
681
1185
  this.emit("request", req, res);
682
1186
  return 0;
683
1187
  }
684
1188
 
1189
+ /* Node's resOnFinish: the request is done with; the next response's turn, or the idle keep-alive
1190
+ * timeout */
1191
+ _resOnFinish(state, req, res) {
1192
+ const i = state.incoming.indexOf(req);
1193
+ if (i !== -1) state.incoming.splice(i, 1);
1194
+ if (state.req === req && req.complete) state.req = null;
1195
+ if (state.res === res) state.res = null;
1196
+ if (res._last || !res._keepAlive) return; /* the socket is ending */
1197
+ const next = state.outgoing.shift();
1198
+ if (next) {
1199
+ state.res = next;
1200
+ next._activate();
1201
+ } else if (this.keepAliveTimeout && state.socket.writable) {
1202
+ state.socket.setTimeout(this.keepAliveTimeout);
1203
+ state.keepAliveTimeoutSet = true;
1204
+ }
1205
+ }
1206
+
685
1207
  /* the parser let go of the socket after an upgrade's head: it's the 'upgrade' listener's */
686
1208
  _onUpgrade(conn, head) {
687
1209
  const state = this._states.get(conn);
@@ -699,6 +1221,7 @@ export class Server extends NetServer {
699
1221
  else socket.destroy();
700
1222
  }
701
1223
  }
1224
+ export const Server = callableClass(ServerClass, "Server"); /* Server(…) without new, as Node's */
702
1225
 
703
1226
  /* ---- Connections: llhttp on a socket's handle, or fed --------------------------------------- */
704
1227
 
@@ -804,7 +1327,10 @@ function clientParsersId() {
804
1327
  res.upgrade = upgrade;
805
1328
  return parserOnIncomingClient(req, socket, res, keepAlive);
806
1329
  },
807
- (conn, chunk) => reqOf(conn)?.res?._push(chunk),
1330
+ (conn, chunk) => {
1331
+ clientSockets.get(conn)?._unrefTimer?.(); /* the socket's activity, as Node's onParserExecute */
1332
+ reqOf(conn)?.res?._push(chunk);
1333
+ },
808
1334
  (conn, trailers) => {
809
1335
  const socket = clientSockets.get(conn);
810
1336
  const req = socket?._httpMessage;
@@ -818,21 +1344,20 @@ function clientParsersId() {
818
1344
  res.rawTrailers = trailers;
819
1345
  for (let i = 0; i < trailers.length; i += 2) addHeader(res.trailers, trailers[i], trailers[i + 1]);
820
1346
  }
821
- if (!statusIsInformational(res.statusCode)) freeParser(socket);
1347
+ if (!statusIsInformational(res.statusCode)) freeParserAfterRead(socket);
822
1348
  res._finish();
823
1349
  },
824
1350
  (conn, head) => {
825
1351
  const socket = clientSockets.get(conn);
826
1352
  if (socket?._httpMessage) clientUpgrade(socket._httpMessage, socket, head);
827
1353
  },
828
- (conn, code, reason) => {
1354
+ (conn, code, reason, bytesParsed, rawPacket) => {
829
1355
  const socket = clientSockets.get(conn);
830
1356
  const req = socket?._httpMessage;
831
- const e = new Error("Parse Error: " + reason);
832
- e.code = code;
833
- e.reason = reason;
1357
+ const e = parseError(code, reason, bytesParsed, rawPacket);
834
1358
  if (socket) {
835
1359
  freeParser(socket);
1360
+ socket.removeListener("end", socketOnEnd); /* as Node's socketOnData */
836
1361
  socket.destroy();
837
1362
  }
838
1363
  if (req) {
@@ -865,10 +1390,25 @@ function freeParser(socket) {
865
1390
  const conn = socket._clientConn;
866
1391
  if (conn === undefined) return;
867
1392
  socket._clientConn = undefined;
1393
+ socket.parser = null; /* as Node's: no parser on a socket between requests */
868
1394
  clientSockets.delete(conn);
869
1395
  httpDetach(conn);
870
1396
  }
871
1397
 
1398
+ /* a complete response's parser, let go of once the read is parsed, as Node's socketOnData frees it:
1399
+ * what follows the response in the read is still parsed, and an error there is the request's (the
1400
+ * socket's entry stays until then) */
1401
+ function freeParserAfterRead(socket) {
1402
+ const conn = socket._clientConn;
1403
+ if (conn === undefined) return;
1404
+ socket._clientConn = undefined;
1405
+ socket.parser = null; /* as Node's: no parser on a socket between requests */
1406
+ httpDetach(conn, true);
1407
+ queueMicrotask(() => {
1408
+ if (clientSockets.get(conn) === socket && socket._clientConn !== conn) clientSockets.delete(conn);
1409
+ });
1410
+ }
1411
+
872
1412
  /* Node's parserOnIncomingClient: 0, or 1 for no body, or 2 to take the socket */
873
1413
  function parserOnIncomingClient(req, socket, res, shouldKeepAlive) {
874
1414
  if (req.res) { /* a second response to one request */
@@ -1026,6 +1566,7 @@ function socketOnEnd() {
1026
1566
  }
1027
1567
 
1028
1568
  const INVALID_PATH_REGEX = /[^!-ÿ]/;
1569
+ const kPath = Symbol("kPath");
1029
1570
 
1030
1571
  export class ClientRequest extends OutgoingMessage {
1031
1572
  constructor(input, options, cb) {
@@ -1042,7 +1583,7 @@ export class ClientRequest extends OutgoingMessage {
1042
1583
  cb = options;
1043
1584
  options = input || {};
1044
1585
  } else {
1045
- options = { ...input, ...options };
1586
+ options = Object.assign({ __proto__: null }, input, options); /* as Node's: no prototype's options */
1046
1587
  }
1047
1588
  let agent = options.agent;
1048
1589
  const defaultAgent = options._defaultAgent || globalAgent;
@@ -1070,7 +1611,7 @@ export class ClientRequest extends OutgoingMessage {
1070
1611
  return host;
1071
1612
  };
1072
1613
  const host = optsWithoutSignal.host = validHost(options.hostname, "hostname") || validHost(options.host, "host") || "localhost";
1073
- const setHost = options.setHost === undefined || Boolean(options.setHost);
1614
+ const setHost = options.setHost !== undefined ? Boolean(options.setHost) : options.setDefaultHeaders !== false;
1074
1615
  this.socketPath = options.socketPath;
1075
1616
  if (options.timeout !== undefined) {
1076
1617
  if (typeof options.timeout !== "number") throw invalidArgType("timeout", "of type number", options.timeout);
@@ -1078,13 +1619,8 @@ export class ClientRequest extends OutgoingMessage {
1078
1619
  }
1079
1620
  const signal = options.signal;
1080
1621
  if (signal) {
1622
+ addAbortSignal(signal, this); /* as Node's: destroyed with an AbortError, now if already aborted */
1081
1623
  delete optsWithoutSignal.signal;
1082
- const onAbort = () => this.destroy(codeError(Error, "ABORT_ERR", "The operation was aborted"));
1083
- if (signal.aborted) process.nextTick(onAbort);
1084
- else {
1085
- signal.addEventListener("abort", onAbort, { once: true });
1086
- this.once("close", () => signal.removeEventListener("abort", onAbort));
1087
- }
1088
1624
  }
1089
1625
  let method = options.method;
1090
1626
  if (method !== null && method !== undefined && typeof method !== "string") throw invalidArgType("options.method", "of type string", method);
@@ -1096,8 +1632,28 @@ export class ClientRequest extends OutgoingMessage {
1096
1632
  }
1097
1633
  if (options.maxHeaderSize !== undefined) validateInteger(options.maxHeaderSize, "maxHeaderSize");
1098
1634
  this.maxHeaderSize = options.maxHeaderSize;
1635
+ const insecureHTTPParser = options.insecureHTTPParser;
1636
+ if (insecureHTTPParser !== undefined && typeof insecureHTTPParser !== "boolean") {
1637
+ throw invalidArgType("options.insecureHTTPParser", "of type boolean", insecureHTTPParser);
1638
+ }
1639
+ this.insecureHTTPParser = insecureHTTPParser;
1640
+ const httpValidation = options.httpValidation;
1641
+ if (httpValidation !== undefined) {
1642
+ if (httpValidation !== "strict" && httpValidation !== "relaxed" && httpValidation !== "insecure") {
1643
+ throw codeError(TypeError, "ERR_INVALID_ARG_VALUE",
1644
+ `The property 'options.httpValidation' must be one of: 'strict', 'relaxed', 'insecure'. Received ${JSON.stringify(httpValidation)}`);
1645
+ }
1646
+ if (insecureHTTPParser !== undefined) {
1647
+ throw codeError(TypeError, "ERR_INVALID_ARG_VALUE",
1648
+ `The property 'options.httpValidation' cannot be used together with options.insecureHTTPParser. Received ${JSON.stringify(httpValidation)}`);
1649
+ }
1650
+ }
1651
+ this.httpValidation = httpValidation;
1652
+ if (options.joinDuplicateHeaders !== undefined && typeof options.joinDuplicateHeaders !== "boolean") {
1653
+ throw invalidArgType("options.joinDuplicateHeaders", "of type boolean", options.joinDuplicateHeaders);
1654
+ }
1099
1655
  this.joinDuplicateHeaders = options.joinDuplicateHeaders;
1100
- this.path = options.path || "/";
1656
+ this[kPath] = options.path || "/";
1101
1657
  if (cb) this.once("response", cb);
1102
1658
  this.useChunkedEncodingByDefault = !(method === "GET" || method === "HEAD" || method === "DELETE" || method === "OPTIONS" ||
1103
1659
  method === "TRACE" || method === "CONNECT");
@@ -1171,191 +1727,19 @@ export class ClientRequest extends OutgoingMessage {
1171
1727
  }
1172
1728
  }
1173
1729
 
1174
- /* Node's _storeHeader: the request line, the headers as set (or as given in an array), then
1175
- * Connection and the body's framing */
1176
- _storeHeader(firstLine, headersArray) {
1177
- let head = firstLine;
1178
- let hasConnection = false, hasLength = false, hasTE = false, hasTrailer = false;
1179
- const add = (name, value) => {
1180
- const key = String(name).toLowerCase();
1181
- if (key === "connection") {
1182
- hasConnection = true;
1183
- if (/close/i.test(String(value))) this._last = true;
1184
- else this.shouldKeepAlive = true;
1185
- } else if (key === "content-length") hasLength = true;
1186
- else if (key === "transfer-encoding") {
1187
- hasTE = true;
1188
- if (/chunked/i.test(String(value))) this.chunkedEncoding = true;
1189
- } else if (key === "trailer") hasTrailer = true;
1190
- if (Array.isArray(value)) for (const v of value) head += name + ": " + v + "\r\n";
1191
- else head += name + ": " + value + "\r\n";
1192
- };
1193
- if (headersArray) {
1194
- if (Array.isArray(headersArray[0])) for (const [name, value] of headersArray) add(name, value);
1195
- else for (let i = 0; i < headersArray.length; i += 2) add(headersArray[i], headersArray[i + 1]);
1196
- } else {
1197
- for (const [, [name, value]] of this._headers) add(name, value);
1198
- }
1199
- if (!hasConnection) {
1200
- const shouldSendKeepAlive = this.shouldKeepAlive && (hasLength || this.useChunkedEncodingByDefault || this.agent);
1201
- if (shouldSendKeepAlive) head += "Connection: keep-alive\r\n";
1202
- else {
1203
- this._last = true;
1204
- head += "Connection: close\r\n";
1205
- }
1206
- }
1207
- if (!hasLength && !hasTE) {
1208
- if (!this._hasBody) this.chunkedEncoding = false;
1209
- else if (!this.useChunkedEncodingByDefault) this._last = true;
1210
- else if (!hasTrailer && !this._removedContLen && typeof this._contentLength === "number") head += "Content-Length: " + this._contentLength + "\r\n";
1211
- else {
1212
- head += "Transfer-Encoding: chunked\r\n";
1213
- this.chunkedEncoding = true;
1214
- }
1215
- }
1216
- this._header = head + "\r\n";
1217
- this._headerSent = false;
1218
- this.headersSent = true;
1219
- }
1220
-
1221
1730
  _implicitHeader() {
1222
1731
  if (this._header) throw codeError(Error, "ERR_HTTP_HEADERS_SENT", "Cannot render headers after they are sent to the client");
1223
1732
  this._storeHeader(this.method + " " + this.path + " HTTP/1.1\r\n", null);
1224
1733
  }
1225
1734
 
1226
- /* bytes for the socket: written now, or kept until it's assigned */
1227
- _writeRaw(data, encoding, callback) {
1228
- const conn = this.socket;
1229
- if (conn?.destroyed) return false;
1230
- if (conn && conn._httpMessage === this && conn.writable) {
1231
- if (this.outputData.length) this._flushOutput(conn);
1232
- return conn.write(data, encoding, callback);
1233
- }
1234
- this.outputData.push({ data, encoding, callback });
1235
- this.outputSize += data.length;
1236
- return this.outputSize < this.writableHighWaterMark;
1237
- }
1238
-
1239
- /* the head goes with the first bytes: in the same string when it can be */
1240
- _send(data, encoding, callback) {
1241
- if (!this._headerSent && this._header !== null) {
1242
- this._headerSent = true;
1243
- if (typeof data === "string" && (encoding === "utf8" || encoding === "latin1" || !encoding)) data = this._header + data;
1244
- else this._writeRaw(this._header, "latin1", null);
1245
- }
1246
- return this._writeRaw(data, encoding, callback);
1247
- }
1248
-
1249
- _flushOutput(socket) {
1250
- const output = this.outputData;
1251
- if (output.length === 0) return undefined;
1252
- this.outputData = [];
1253
- this.outputSize = 0;
1254
- let ret;
1255
- for (const { data, encoding, callback } of output) ret = socket.write(data, encoding, callback);
1256
- return ret;
1257
- }
1258
-
1259
- _flush() {
1260
- const socket = this.socket;
1261
- if (socket?.writable) {
1262
- const ret = this._flushOutput(socket);
1263
- if (!this.finished && ret && this._needDrain) {
1264
- this._needDrain = false;
1265
- this.emit("drain");
1266
- }
1267
- }
1268
- }
1269
-
1270
- write(chunk, encoding, callback) {
1271
- if (typeof encoding === "function") {
1272
- callback = encoding;
1273
- encoding = null;
1274
- }
1275
- const ret = this._write_(chunk, encoding, callback, false);
1276
- if (!ret) this._needDrain = true;
1277
- return ret;
1278
- }
1279
-
1280
- _write_(chunk, encoding, callback, fromEnd) {
1281
- if (typeof callback !== "function") callback = () => {};
1282
- if (chunk === null) throw codeError(TypeError, "ERR_STREAM_NULL_VALUES", "May not write null values to stream");
1283
- if (typeof chunk !== "string" && !(chunk instanceof Uint8Array)) {
1284
- throw invalidArgType("chunk", "of type string or an instance of Buffer or Uint8Array", chunk);
1285
- }
1286
- let err;
1287
- if (this.finished) err = codeError(Error, "ERR_STREAM_WRITE_AFTER_END", "write after end");
1288
- else if (this.destroyed) err = codeError(Error, "ERR_STREAM_DESTROYED", "Cannot call write after a stream was destroyed");
1289
- if (err) {
1290
- if (!this.destroyed) process.nextTick(() => this.emit("error", err));
1291
- process.nextTick(callback, err);
1292
- return false;
1293
- }
1294
- const len = typeof chunk === "string" ? Buffer.byteLength(chunk, encoding || "utf8") : chunk.byteLength;
1295
- if (!this._header) {
1296
- if (fromEnd) this._contentLength = len;
1297
- this._implicitHeader();
1298
- }
1299
- if (!this._hasBody) {
1300
- process.nextTick(callback);
1301
- return true;
1302
- }
1303
- if (this.chunkedEncoding && len !== 0) {
1304
- this._send(len.toString(16), "latin1", null);
1305
- this._send("\r\n", "latin1", null);
1306
- this._send(chunk, encoding, null);
1307
- return this._send("\r\n", "latin1", callback);
1308
- }
1309
- return this._send(chunk, encoding, callback);
1310
- }
1311
-
1312
- end(chunk, encoding, callback) {
1313
- if (typeof chunk === "function") {
1314
- callback = chunk;
1315
- chunk = null;
1316
- encoding = null;
1317
- } else if (typeof encoding === "function") {
1318
- callback = encoding;
1319
- encoding = null;
1320
- }
1321
- if (chunk) {
1322
- if (this.finished) {
1323
- this._write_(chunk, encoding, null, true);
1324
- return this;
1325
- }
1326
- this._write_(chunk, encoding, null, true);
1327
- } else if (this.finished) {
1328
- if (typeof callback === "function") {
1329
- if (!this.writableFinished) this.on("finish", callback);
1330
- else callback(codeError(Error, "ERR_STREAM_ALREADY_FINISHED", "Cannot call end after a stream was finished"));
1331
- }
1332
- return this;
1333
- } else if (!this._header) {
1334
- this._contentLength = 0;
1335
- this._implicitHeader();
1336
- }
1337
- if (typeof callback === "function") this.once("finish", callback);
1338
- const finish = () => {
1339
- if (this.socket && this.socket._hadError) return;
1340
- this.writableFinished = true;
1341
- this.emit("finish");
1342
- };
1343
- if (this._hasBody && this.chunkedEncoding) this._send("0\r\n" + this._trailer + "\r\n", "latin1", finish);
1344
- else if (!this._headerSent || chunk) this._send("", "latin1", finish);
1345
- else process.nextTick(finish);
1346
- this.finished = true;
1347
- this.writableEnded = true;
1348
- this.writable = false;
1349
- return this;
1350
- }
1351
-
1352
- flushHeaders() {
1353
- if (!this._header) this._implicitHeader();
1354
- this._send("");
1355
- }
1356
-
1357
1735
  /* the socket the agent (or createConnection) gave: its listeners, the parser, then the output */
1358
1736
  onSocket(socket, err) {
1737
+ /* as Node's: the socket's request and error listener set now, so an error before the next tick
1738
+ * reaches the request, and a request destroyed meanwhile gives its socket back to the agent */
1739
+ if (socket && !err) {
1740
+ socket._httpMessage = this;
1741
+ socket.on("error", socketErrorListener);
1742
+ }
1359
1743
  process.nextTick(() => {
1360
1744
  if (this.destroyed || err) {
1361
1745
  this.destroyed = true;
@@ -1368,6 +1752,7 @@ export class ClientRequest extends OutgoingMessage {
1368
1752
  if (socket) {
1369
1753
  if (!err && this.agent && !socket.destroyed) {
1370
1754
  socket.emit("free");
1755
+ socket.removeListener("error", socketErrorListener);
1371
1756
  done(this._error);
1372
1757
  } else {
1373
1758
  socket.once("close", () => done(err || this._error));
@@ -1379,8 +1764,6 @@ export class ClientRequest extends OutgoingMessage {
1379
1764
  return;
1380
1765
  }
1381
1766
  this.socket = socket;
1382
- socket._httpMessage = this;
1383
- socket.on("error", socketErrorListener);
1384
1767
  socket.on("end", socketOnEnd);
1385
1768
  socket.on("close", socketCloseListener);
1386
1769
  if (this.timeout !== undefined || this.agent?.options?.timeout) listenSocketTimeout(this);
@@ -1406,6 +1789,7 @@ export class ClientRequest extends OutgoingMessage {
1406
1789
  this.destroyed = true;
1407
1790
  if (this.res) this.res._dump();
1408
1791
  this._error = err;
1792
+ this._errored = err;
1409
1793
  this.socket?.destroy(err);
1410
1794
  return this;
1411
1795
  }
@@ -1439,6 +1823,21 @@ export class ClientRequest extends OutgoingMessage {
1439
1823
  }
1440
1824
  }
1441
1825
 
1826
+ /* the request path, checked when set too: a path changed after the request was made can't smuggle
1827
+ * a header in (Node's) */
1828
+ Object.defineProperty(ClientRequest.prototype, "path", {
1829
+ get() {
1830
+ return this[kPath];
1831
+ },
1832
+ set(value) {
1833
+ const path = String(value);
1834
+ if (INVALID_PATH_REGEX.test(path)) throw codeError(TypeError, "ERR_UNESCAPED_CHARACTERS", "Request path contains unescaped characters");
1835
+ this[kPath] = path;
1836
+ },
1837
+ configurable: true,
1838
+ enumerable: true,
1839
+ });
1840
+
1442
1841
  export function request(url, options, cb) {
1443
1842
  return new ClientRequest(url, options, cb);
1444
1843
  }
@@ -1461,6 +1860,13 @@ export function setMaxIdleHTTPParsers(max) {
1461
1860
  export { Agent, globalAgent };
1462
1861
 
1463
1862
  export default {
1464
- METHODS, STATUS_CODES, Agent, ClientRequest, IncomingMessage, OutgoingMessage, ServerResponse, Server, createServer, get, globalAgent,
1863
+ METHODS, STATUS_CODES, Agent, ClientRequest, IncomingMessage, OutgoingMessage, ServerResponse, Server, createServer, get,
1465
1864
  maxHeaderSize, request, setMaxIdleHTTPParsers, validateHeaderName, validateHeaderValue,
1865
+ /* an accessor, as Node's: a program may replace the agent requests use by default */
1866
+ get globalAgent() {
1867
+ return globalAgent;
1868
+ },
1869
+ set globalAgent(value) {
1870
+ setGlobalAgent(value);
1871
+ },
1466
1872
  };