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.
- package/README.md +6 -3
- package/THIRD_PARTY_LICENSES +1 -1
- package/cli.js +55 -4
- package/node/prelude/_http_agent.js +5 -0
- package/node/prelude/_http_server.js +7 -0
- package/node/prelude/dns.js +7 -1
- package/node/prelude/events.js +7 -3
- package/node/prelude/http.js +685 -279
- package/node/prelude/https.js +14 -3
- package/node/prelude/internal/events_extras.js +2 -5
- package/node/prelude/internal/features.js +17 -0
- package/node/prelude/internal/http_agent.js +88 -2
- package/node/prelude/internal/http_symbols.js +3 -0
- package/node/prelude/internal/net_bound.js +123 -0
- package/node/prelude/internal/net_onread.js +73 -0
- package/node/prelude/internal/net_tos.js +54 -0
- package/node/prelude/internal/perf_observe.js +7 -1
- package/node/prelude/internal/tls/wrap.js +2 -1
- package/node/prelude/module.js +42 -18
- package/node/prelude/net.js +195 -48
- package/node/prelude/perf_hooks.js +4 -0
- package/node/src/host_http.c +151 -21
- package/node/src/host_net.c +75 -9
- package/package.json +1 -1
- package/prebuilt/win32-x64/boringssl-ce573858472b7fc9.a +0 -0
- package/prebuilt/win32-x64/brotli-20d86a61f30baeb3.a +0 -0
- package/prebuilt/win32-x64/uv-b11bae65d7fced82.a +0 -0
- package/prebuilt/win32-x64/zlib-4ce2317a4c3f5fb5.a +0 -0
- package/prebuilt/win32-x64/zstd-015315ac7c784c73.a +0 -0
- package/runtime/include/js_atoms.h +4 -0
- package/runtime/include/js_web.h +3 -0
- package/runtime/include/jscontext.h +8 -0
- package/runtime/src/atoms.c +37 -3
- package/runtime/src/events.c +19 -0
- package/runtime/src/gc.c +4 -4
- package/runtime/src/internal.h +4 -4
- package/runtime/src/regexp.c +569 -12
- package/runtime/src/shape.c +68 -1
- package/runtime/src/weak.c +95 -24
package/node/prelude/http.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
else
|
|
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.
|
|
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
|
-
|
|
290
|
-
this.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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)
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
791
|
+
this._send(chunkLength(chunk).toString(16) + "\r\n");
|
|
792
|
+
this._send(chunk);
|
|
793
|
+
this._send("\r\n");
|
|
425
794
|
} else {
|
|
426
|
-
|
|
795
|
+
this._send(chunk);
|
|
427
796
|
}
|
|
428
797
|
}
|
|
429
798
|
if (typeof callback === "function") queueMicrotask(callback);
|
|
430
|
-
return
|
|
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)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (
|
|
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)
|
|
455
|
-
if (this.chunkedEncoding && !this._head)
|
|
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 (
|
|
460
|
-
|
|
461
|
-
this
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
540
|
-
if (this.
|
|
541
|
-
this.
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
614
|
-
|
|
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
|
|
1114
|
+
const responses = state.res ? [state.res, ...state.outgoing] : state.outgoing;
|
|
620
1115
|
state.res = null;
|
|
621
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
667
|
-
|
|
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) =>
|
|
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))
|
|
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 =
|
|
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 = {
|
|
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
|
|
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
|
|
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,
|
|
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
|
};
|