visaljs 0.0.8 → 0.0.10

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