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.
- package/README.md +6 -3
- package/THIRD_PARTY_LICENSES +1 -1
- package/cli.js +2782 -770
- package/node/include/js_node.h +1 -0
- 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 +693 -285
- 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_chars.ts +22 -0
- 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 +212 -61
- 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/node/src/host_sys.c +9 -0
- 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_buffer.h +185 -0
- package/runtime/include/js_builtins.h +53 -0
- package/runtime/include/js_exception.h +3 -0
- package/runtime/include/js_function.h +1 -1
- package/runtime/include/js_object.h +21 -3
- package/runtime/include/js_ops.h +27 -0
- package/runtime/include/js_runtime.h +1 -0
- package/runtime/include/js_web.h +3 -3
- package/runtime/include/jscontext.h +15 -0
- package/runtime/src/atoms.c +37 -3
- package/runtime/src/builtins.c +17 -4
- package/runtime/src/events.c +19 -0
- package/runtime/src/function.c +37 -10
- package/runtime/src/gc.c +7 -5
- package/runtime/src/internal.h +4 -4
- package/runtime/src/iterator.c +5 -1
- package/runtime/src/object.c +42 -6
- package/runtime/src/regexp.c +569 -12
- package/runtime/src/shape.c +68 -1
- package/runtime/src/typedarray.c +23 -0
- package/runtime/src/weak.c +95 -24
package/node/prelude/http.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
else
|
|
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" || !
|
|
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 (
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
290
|
-
this.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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)
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
790
|
+
this._send(chunkLength(chunk).toString(16) + "\r\n");
|
|
791
|
+
this._send(chunk);
|
|
792
|
+
this._send("\r\n");
|
|
425
793
|
} else {
|
|
426
|
-
|
|
794
|
+
this._send(chunk);
|
|
427
795
|
}
|
|
428
796
|
}
|
|
429
797
|
if (typeof callback === "function") queueMicrotask(callback);
|
|
430
|
-
return
|
|
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)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (
|
|
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)
|
|
455
|
-
if (this.chunkedEncoding && !this._head)
|
|
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 (
|
|
460
|
-
|
|
461
|
-
this
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
614
|
-
|
|
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
|
|
1113
|
+
const responses = state.res ? [state.res, ...state.outgoing] : state.outgoing;
|
|
620
1114
|
state.res = null;
|
|
621
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
667
|
-
|
|
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) =>
|
|
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))
|
|
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 =
|
|
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 = {
|
|
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
|
|
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 (!
|
|
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
|
|
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,
|
|
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
|
};
|