fulmine.js 5.21.3 → 5.21.4
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/package.json +5 -1
- package/src/adopt.js +4 -1
- package/src/application.js +11 -2
- package/src/cli.js +1 -0
- package/src/compression.js +40 -3
- package/src/declarative.js +9 -5
- package/src/lazy-readable.js +15 -15
- package/src/lazy-writable.js +15 -15
- package/src/middlewares.js +15 -7
- package/src/nest.js +2 -0
- package/src/optimizer.js +65 -7
- package/src/options.d.ts +1 -1
- package/src/request.js +22 -16
- package/src/response-utils.js +41 -4
- package/src/response.js +66 -55
- package/src/route.js +5 -3
- package/src/router-utils.js +40 -5
- package/src/router.js +23 -9
- package/src/server-shape.js +3 -3
- package/src/server-timing.js +21 -0
- package/src/testing.js +2 -1
- package/src/usage.js +1 -1
- package/src/utils.js +4 -1
- package/src/verify.js +2 -0
- package/src/view.js +1 -0
- package/src/websocket.js +2 -1
- package/src/work.js +1 -0
package/src/request.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
|
|
44
44
|
// whose headers #collectHeader is filling: uWS's forEach is synchronous, so one callback serves
|
|
45
45
|
// every request
|
|
46
|
+
/** @type {import("./request.js")|null} */
|
|
46
47
|
let currentRequest = null;
|
|
47
48
|
|
|
48
49
|
module.exports = class Request extends LazyReadable {
|
|
@@ -83,6 +84,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
83
84
|
/** @type {string[]|null} */
|
|
84
85
|
#cachedSubdomains = null;
|
|
85
86
|
|
|
87
|
+
/** The socket stand-in `req.connection` once built, see the getter. @type {import("./socket.js")|null} */
|
|
88
|
+
#cachedConnection = null;
|
|
89
|
+
|
|
90
|
+
/** Behind `req.signal`, made on the first read. @type {AbortController|undefined} */
|
|
91
|
+
#abortController;
|
|
92
|
+
|
|
86
93
|
/**
|
|
87
94
|
* Copies one header out of uWS and notices what the constructor decides by. One function for
|
|
88
95
|
* every request through currentRequest, an arrow per request cost a closure.
|
|
@@ -91,7 +98,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
91
98
|
* @param {string} value
|
|
92
99
|
*/
|
|
93
100
|
static #collectHeader = (headerKey, value) => {
|
|
94
|
-
|
|
101
|
+
// set by the two forEach callers, never null while uWS is calling back
|
|
102
|
+
const r = /** @type {Request} */ (currentRequest);
|
|
95
103
|
r.#rawHeadersEntries.push(headerKey, value);
|
|
96
104
|
// the response, built right after, must not answer keep-alive to a client that said close
|
|
97
105
|
if (headerKey.length === 10 && headerKey === "connection" && saysClose(value)) {
|
|
@@ -321,7 +329,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
321
329
|
/** The verbs a path answers, for an OPTIONS; null on every other method. @type {Set<string>|null} */
|
|
322
330
|
_matchedMethods = null;
|
|
323
331
|
|
|
324
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* What each app.param() callback was called with, per router, made on first use.
|
|
334
|
+
* @type {Map<Map<string, Function[]>, Map<string, import("./router.js").ParamCall>>|null}
|
|
335
|
+
*/
|
|
325
336
|
_paramCalled = null;
|
|
326
337
|
|
|
327
338
|
/**
|
|
@@ -573,9 +584,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
573
584
|
return this.res?.finished || this.res?.aborted;
|
|
574
585
|
}
|
|
575
586
|
|
|
576
|
-
/** @type {AbortController|undefined} */
|
|
577
|
-
#abortController;
|
|
578
|
-
|
|
579
587
|
/**
|
|
580
588
|
* node's `req.signal`, fired when the request is over: `@angular/ssr` reads it to give up a
|
|
581
589
|
* render. Made on the first ask.
|
|
@@ -630,7 +638,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
630
638
|
return this;
|
|
631
639
|
}
|
|
632
640
|
|
|
633
|
-
/**
|
|
641
|
+
/**
|
|
642
|
+
* Readable's pull: uWS pushes, so this only lifts the backpressure a full queue put on it.
|
|
643
|
+
* @override
|
|
644
|
+
*/
|
|
634
645
|
_read() {
|
|
635
646
|
// first, so a bodyless stream still ends for a consumer arriving after the response
|
|
636
647
|
if (this.#emptyBody) {
|
|
@@ -1011,9 +1022,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
1011
1022
|
return ip;
|
|
1012
1023
|
}
|
|
1013
1024
|
|
|
1014
|
-
/** @type {import("./socket.js")|null} */
|
|
1015
|
-
#cachedConnection = null;
|
|
1016
|
-
|
|
1017
1025
|
/**
|
|
1018
1026
|
* The socket stand-in, the same object as `res.socket`, kept here so it still answers once
|
|
1019
1027
|
* the response is over and `res.socket` is null.
|
|
@@ -1214,8 +1222,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
1214
1222
|
*
|
|
1215
1223
|
* @param {number} size length of the resource being served
|
|
1216
1224
|
* @param {{combine?: boolean}} [options] combine adjacent and overlapping ranges
|
|
1217
|
-
* @returns {
|
|
1218
|
-
* or undefined when there is no Range header
|
|
1225
|
+
* @returns {import("range-parser").Ranges|-1|-2|undefined} the ranges, -1 when unsatisfiable,
|
|
1226
|
+
* -2 when malformed, or undefined when there is no Range header
|
|
1219
1227
|
*/
|
|
1220
1228
|
range(size, options) {
|
|
1221
1229
|
const range = this.headers["range"];
|
|
@@ -1302,10 +1310,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
1302
1310
|
return this.#rawHeadersEntries.slice();
|
|
1303
1311
|
}
|
|
1304
1312
|
|
|
1305
|
-
// the three below report work this request was made to do, for src/work.js
|
|
1306
|
-
|
|
1307
1313
|
/**
|
|
1308
|
-
* Whether the folded `req.headers` object was built.
|
|
1314
|
+
* Whether the folded `req.headers` object was built, which src/work.js reports.
|
|
1309
1315
|
* @returns {boolean}
|
|
1310
1316
|
*/
|
|
1311
1317
|
get _headersBuilt() {
|
|
@@ -1313,7 +1319,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1313
1319
|
}
|
|
1314
1320
|
|
|
1315
1321
|
/**
|
|
1316
|
-
* Whether the query string was parsed at least once.
|
|
1322
|
+
* Whether the query string was parsed at least once, which src/work.js reports.
|
|
1317
1323
|
* @returns {boolean}
|
|
1318
1324
|
*/
|
|
1319
1325
|
get _queryParsed() {
|
|
@@ -1321,7 +1327,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1321
1327
|
}
|
|
1322
1328
|
|
|
1323
1329
|
/**
|
|
1324
|
-
* Whether the socket stand-in `req.connection` was allocated.
|
|
1330
|
+
* Whether the socket stand-in `req.connection` was allocated, which src/work.js reports.
|
|
1325
1331
|
* @returns {boolean}
|
|
1326
1332
|
*/
|
|
1327
1333
|
get _socketBuilt() {
|
package/src/response-utils.js
CHANGED
|
@@ -38,9 +38,12 @@ const kShapeMode =
|
|
|
38
38
|
const VALIDATED_HEADER_NAMES = new Map();
|
|
39
39
|
// The names and values that recur on every response, kept as Buffers for the uWS crossing: a Buffer
|
|
40
40
|
// is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is not here
|
|
41
|
-
// crosses as the string it was. Names must stay lowercase, as writeHeaders receives them.
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// crosses as the string it was. Names must stay lowercase, as writeHeaders receives them. The
|
|
42
|
+
// casts are for the __proto__ key, which the type checker reads as a property
|
|
43
|
+
/** @type {Record<string, Buffer>} */
|
|
44
|
+
const HEADER_NAME_BUF = /** @type {any} */ ({ __proto__: null });
|
|
45
|
+
/** @type {Record<string, Buffer>} */
|
|
46
|
+
const HEADER_VALUE_BUF = /** @type {any} */ ({ __proto__: null });
|
|
44
47
|
for (const s of ["connection", "keep-alive", "content-type", "vary", "x-powered-by", "content-encoding"]) {
|
|
45
48
|
HEADER_NAME_BUF[s] = Buffer.from(s);
|
|
46
49
|
}
|
|
@@ -81,11 +84,45 @@ function statusLine(code, text) {
|
|
|
81
84
|
return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The TypeError node throws for a chunk that is not a string, a Buffer or a Uint8Array, worded as
|
|
89
|
+
* node words it.
|
|
90
|
+
*
|
|
91
|
+
* @param {unknown} chunk what end() was handed, known here not to be a string or a Uint8Array
|
|
92
|
+
* @returns {NodeJS.ErrnoException}
|
|
93
|
+
*/
|
|
94
|
+
function invalidChunkError(chunk) {
|
|
95
|
+
let received;
|
|
96
|
+
if (chunk === null) {
|
|
97
|
+
received = "null";
|
|
98
|
+
} else if (typeof chunk === "object" || typeof chunk === "function") {
|
|
99
|
+
const name = /** @type {any} */ (chunk).constructor?.name;
|
|
100
|
+
received = name ? `an instance of ${name}` : "an instance of Object";
|
|
101
|
+
} else if (typeof chunk === "bigint") {
|
|
102
|
+
received = `type bigint (${chunk}n)`;
|
|
103
|
+
} else if (typeof chunk === "symbol") {
|
|
104
|
+
received = `type symbol (${String(chunk)})`;
|
|
105
|
+
} else {
|
|
106
|
+
received = `type ${typeof chunk} (${chunk})`;
|
|
107
|
+
}
|
|
108
|
+
/** @type {NodeJS.ErrnoException} */
|
|
109
|
+
const err = new TypeError(
|
|
110
|
+
`The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received ${received}`
|
|
111
|
+
);
|
|
112
|
+
err.code = "ERR_INVALID_ARG_TYPE";
|
|
113
|
+
// as node: the bracketed name goes into the stack's first line, then err.name reads TypeError
|
|
114
|
+
err.name = "TypeError [ERR_INVALID_ARG_TYPE]";
|
|
115
|
+
void err.stack;
|
|
116
|
+
delete (/** @type {Record<string, unknown>} */ (/** @type {unknown} */ (err)).name);
|
|
117
|
+
return err;
|
|
118
|
+
}
|
|
119
|
+
|
|
84
120
|
module.exports = {
|
|
85
121
|
kOutHeaders,
|
|
86
122
|
kShapeMode,
|
|
87
123
|
VALIDATED_HEADER_NAMES,
|
|
88
124
|
HEADER_NAME_BUF,
|
|
89
125
|
HEADER_VALUE_BUF,
|
|
90
|
-
statusLine
|
|
126
|
+
statusLine,
|
|
127
|
+
invalidChunkError
|
|
91
128
|
};
|
package/src/response.js
CHANGED
|
@@ -49,39 +49,6 @@ const { isAbsolute } = require("path");
|
|
|
49
49
|
const fs = require("fs");
|
|
50
50
|
const Path = require("path");
|
|
51
51
|
const statuses = require("statuses");
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The TypeError node throws for a chunk that is not a string, a Buffer or a Uint8Array, worded as
|
|
55
|
-
* node words it.
|
|
56
|
-
*
|
|
57
|
-
* @param {unknown} chunk what end() was handed, known here not to be a string or a Uint8Array
|
|
58
|
-
* @returns {NodeJS.ErrnoException}
|
|
59
|
-
*/
|
|
60
|
-
function invalidChunkError(chunk) {
|
|
61
|
-
let received;
|
|
62
|
-
if (chunk === null) {
|
|
63
|
-
received = "null";
|
|
64
|
-
} else if (typeof chunk === "object" || typeof chunk === "function") {
|
|
65
|
-
const name = /** @type {any} */ (chunk).constructor?.name;
|
|
66
|
-
received = name ? `an instance of ${name}` : "an instance of Object";
|
|
67
|
-
} else if (typeof chunk === "bigint") {
|
|
68
|
-
received = `type bigint (${chunk}n)`;
|
|
69
|
-
} else if (typeof chunk === "symbol") {
|
|
70
|
-
received = `type symbol (${String(chunk)})`;
|
|
71
|
-
} else {
|
|
72
|
-
received = `type ${typeof chunk} (${chunk})`;
|
|
73
|
-
}
|
|
74
|
-
/** @type {NodeJS.ErrnoException} */
|
|
75
|
-
const err = new TypeError(
|
|
76
|
-
`The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received ${received}`
|
|
77
|
-
);
|
|
78
|
-
err.code = "ERR_INVALID_ARG_TYPE";
|
|
79
|
-
// as node: the bracketed name goes into the stack's first line, then err.name reads TypeError
|
|
80
|
-
err.name = "TypeError [ERR_INVALID_ARG_TYPE]";
|
|
81
|
-
void err.stack;
|
|
82
|
-
delete (/** @type {Record<string, unknown>} */ (/** @type {unknown} */ (err)).name);
|
|
83
|
-
return err;
|
|
84
|
-
}
|
|
85
52
|
const { sign } = require("cookie-signature");
|
|
86
53
|
const ms = require("ms");
|
|
87
54
|
const Socket = require("./socket.js");
|
|
@@ -92,7 +59,8 @@ const {
|
|
|
92
59
|
VALIDATED_HEADER_NAMES,
|
|
93
60
|
HEADER_NAME_BUF,
|
|
94
61
|
HEADER_VALUE_BUF,
|
|
95
|
-
statusLine
|
|
62
|
+
statusLine,
|
|
63
|
+
invalidChunkError
|
|
96
64
|
} = require("./response-utils.js");
|
|
97
65
|
|
|
98
66
|
// How much a chunked response may gather before it goes to uWS: 33 KB in 500 pieces measured
|
|
@@ -128,6 +96,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
128
96
|
/** @type {((err?: Error|null) => void)|null} */
|
|
129
97
|
#pendingCallback = null;
|
|
130
98
|
|
|
99
|
+
/**
|
|
100
|
+
* An end() that arrived while uWS still held a chunk, run once that chunk is through. Not
|
|
101
|
+
* waited for on 'drain': node emits it only after a write() that said false, and a piece
|
|
102
|
+
* under the high-water mark never says it, so a stream ending on a small piece a slow client
|
|
103
|
+
* had not taken yet was never closed (compression-file under load, 2026-09-21).
|
|
104
|
+
* @type {(() => void)|null}
|
|
105
|
+
*/
|
|
106
|
+
#deferredEnd = null;
|
|
107
|
+
|
|
131
108
|
/**
|
|
132
109
|
* Chunks written but not yet handed to uWS: a write costs uWS everything buffered behind it, so
|
|
133
110
|
* many small pieces cost quadratically, 500 writes of 66 bytes measured 13ms against 0.4ms in
|
|
@@ -333,6 +310,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
333
310
|
* A finished or aborted response only tears the stream down, touching an aborted uWS response
|
|
334
311
|
* is a use after free. writableEnded reads true after this, node keeps it false until end().
|
|
335
312
|
*
|
|
313
|
+
* @override
|
|
336
314
|
* @param {Error} [error] whatever the caller is destroying the response with
|
|
337
315
|
* @returns {this}
|
|
338
316
|
*/
|
|
@@ -382,8 +360,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
382
360
|
get [kOutHeaders]() {
|
|
383
361
|
if (!this.#outHeaders) {
|
|
384
362
|
this.#outHeaders = new Proxy(this.headers, {
|
|
363
|
+
// node keys the slot by lowercased name, and stores [name, value]
|
|
385
364
|
set: (obj, prop, value) => {
|
|
386
|
-
this.set(prop, value[1]);
|
|
365
|
+
this.set(/** @type {string} */ (prop), value[1]);
|
|
387
366
|
return true;
|
|
388
367
|
},
|
|
389
368
|
get: (obj, prop) => {
|
|
@@ -394,6 +373,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
394
373
|
return this.#outHeaders;
|
|
395
374
|
}
|
|
396
375
|
|
|
376
|
+
/** @param {Response["headers"]|null} value */
|
|
397
377
|
set [kOutHeaders](value) {
|
|
398
378
|
this.#outHeaders = value;
|
|
399
379
|
}
|
|
@@ -456,6 +436,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
456
436
|
this.#pendingCallback = null;
|
|
457
437
|
this.writingChunk = false;
|
|
458
438
|
if (cb) cb(null);
|
|
439
|
+
this.#afterPending();
|
|
459
440
|
return true;
|
|
460
441
|
});
|
|
461
442
|
} else {
|
|
@@ -464,6 +445,18 @@ module.exports = class Response extends LazyWritable {
|
|
|
464
445
|
}
|
|
465
446
|
}
|
|
466
447
|
|
|
448
|
+
/**
|
|
449
|
+
* After a held chunk went through and the stream's callback ran: the callback may have pushed
|
|
450
|
+
* the next buffered piece into uWS's hands, so the waiting end() runs only when nothing is.
|
|
451
|
+
*/
|
|
452
|
+
#afterPending() {
|
|
453
|
+
if (this.#deferredEnd !== null && !this.writingChunk) {
|
|
454
|
+
const end = this.#deferredEnd;
|
|
455
|
+
this.#deferredEnd = null;
|
|
456
|
+
end();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
467
460
|
/**
|
|
468
461
|
* The booked flush. Static, so a response that never writes in pieces allocates no closure.
|
|
469
462
|
*
|
|
@@ -479,6 +472,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
479
472
|
* Writable's sink: the head if not out yet, then the chunk to uWS through the queue for a
|
|
480
473
|
* chunked response or tryEnd with a Content-Length. Backpressure defers the callback.
|
|
481
474
|
*
|
|
475
|
+
* @override
|
|
482
476
|
* @param {any} chunk whatever a Writable was handed
|
|
483
477
|
* @param {BufferEncoding} encoding
|
|
484
478
|
* @param {(err?: Error|null) => void} callback
|
|
@@ -488,11 +482,13 @@ module.exports = class Response extends LazyWritable {
|
|
|
488
482
|
/** @type {NodeJS.ErrnoException} */
|
|
489
483
|
const err = new Error("Request aborted");
|
|
490
484
|
err.code = "ECONNABORTED";
|
|
491
|
-
|
|
485
|
+
this.destroy(err);
|
|
486
|
+
return;
|
|
492
487
|
}
|
|
493
488
|
if (this.finished) {
|
|
494
489
|
const err = new Error("Response already finished");
|
|
495
|
-
|
|
490
|
+
this.destroy(err);
|
|
491
|
+
return;
|
|
496
492
|
}
|
|
497
493
|
|
|
498
494
|
this.writingChunk = true;
|
|
@@ -554,6 +550,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
554
550
|
this.writingChunk = false;
|
|
555
551
|
handlerUsed = true;
|
|
556
552
|
callback(null);
|
|
553
|
+
this.#afterPending();
|
|
557
554
|
}
|
|
558
555
|
return ok;
|
|
559
556
|
});
|
|
@@ -697,6 +694,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
697
694
|
}
|
|
698
695
|
|
|
699
696
|
/**
|
|
697
|
+
* @override
|
|
700
698
|
* @param {string|Buffer|Uint8Array|null|(() => void)} [data] the last body piece, or the callback in
|
|
701
699
|
* node's one-argument shape
|
|
702
700
|
* @param {BufferEncoding|(() => void)} [encoding] how a string body is encoded, or the callback in
|
|
@@ -726,13 +724,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
726
724
|
}
|
|
727
725
|
// uWS takes a string as utf-8 only: res.end(data, "binary") is how old code sends an image
|
|
728
726
|
if (typeof data === "string" && encoding !== undefined && encoding !== "utf8" && encoding !== "utf-8") {
|
|
729
|
-
data = Buffer.from(data, encoding);
|
|
727
|
+
data = Buffer.from(data, /** @type {BufferEncoding} */ (encoding));
|
|
730
728
|
}
|
|
731
729
|
|
|
732
730
|
if (this.writingChunk) {
|
|
733
|
-
this
|
|
734
|
-
this.end(data, cb);
|
|
735
|
-
});
|
|
731
|
+
this.#deferredEnd = () => this.end(data, cb);
|
|
736
732
|
return this;
|
|
737
733
|
}
|
|
738
734
|
if (this.finished) {
|
|
@@ -743,10 +739,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
743
739
|
this.writeHead(this.statusCode);
|
|
744
740
|
}
|
|
745
741
|
// uWS corks itself for the synchronous window of its route handler, see _corkNeeded
|
|
742
|
+
// by now a function was moved to cb, which the editor's config cannot see
|
|
743
|
+
const chunk = /** @type {string|Uint8Array|null|undefined} */ (data);
|
|
746
744
|
if (this._corkNeeded) {
|
|
747
|
-
this._res.cork(() => this._finish(
|
|
745
|
+
this._res.cork(() => this._finish(chunk, cb));
|
|
748
746
|
} else {
|
|
749
|
-
this._finish(
|
|
747
|
+
this._finish(chunk, cb);
|
|
750
748
|
}
|
|
751
749
|
return this;
|
|
752
750
|
}
|
|
@@ -823,7 +821,8 @@ module.exports = class Response extends LazyWritable {
|
|
|
823
821
|
* Sends the body, with a Content-Type when none was set and an ETag when the setting asks.
|
|
824
822
|
* A number is a value to serialise, never a status: that is `sendStatus()`.
|
|
825
823
|
*
|
|
826
|
-
* @param {string|number|boolean|object|
|
|
824
|
+
* @param {string|number|boolean|object|null} [body] a Buffer or a Uint8Array is sent as bytes,
|
|
825
|
+
* any other object is serialised
|
|
827
826
|
* @returns {this}
|
|
828
827
|
*/
|
|
829
828
|
send(body) {
|
|
@@ -840,6 +839,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
840
839
|
delete this.headers["content-length"];
|
|
841
840
|
delete this.headers["transfer-encoding"];
|
|
842
841
|
}
|
|
842
|
+
} else if (this.req.fresh || this.statusCode === 204 || this.statusCode === 304) {
|
|
843
|
+
// express strips the content headers through removeHeader, which node refuses
|
|
844
|
+
// once the head is out; a fresh request is made a 304 first and lands here too
|
|
845
|
+
throw headersSentError("remove");
|
|
846
|
+
} else if (this.statusCode === 205) {
|
|
847
|
+
throw headersSentError("set");
|
|
843
848
|
}
|
|
844
849
|
return this.end("");
|
|
845
850
|
}
|
|
@@ -866,11 +871,12 @@ module.exports = class Response extends LazyWritable {
|
|
|
866
871
|
} else if (!isBuffer && typeof body !== "string") {
|
|
867
872
|
// a symbol, a bigint or a function: what node's byteLength or from() throws is the
|
|
868
873
|
// answer. A string never gets here, measuring it twice cost 157us per thousand requests
|
|
874
|
+
const unsendable = /** @type {any} */ (body);
|
|
869
875
|
const generateETag = !this.headers["etag"] && typeof this.app._hot().etagFn === "function";
|
|
870
|
-
if (!generateETag &&
|
|
871
|
-
Buffer.byteLength(
|
|
876
|
+
if (!generateETag && unsendable.length < 1000) {
|
|
877
|
+
Buffer.byteLength(unsendable, "utf8");
|
|
872
878
|
}
|
|
873
|
-
Buffer.from(
|
|
879
|
+
Buffer.from(unsendable, "utf8");
|
|
874
880
|
}
|
|
875
881
|
if (typeof body === "string" && !isBuffer) {
|
|
876
882
|
const contentType = this.headers["content-type"];
|
|
@@ -898,7 +904,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
898
904
|
!this.req.noEtag &&
|
|
899
905
|
(hot.etagMethods === null || hot.etagMethods.has(this.req.method))
|
|
900
906
|
) {
|
|
901
|
-
const etag = etagFn(body);
|
|
907
|
+
const etag = etagFn(/** @type {string|Buffer} */ (body));
|
|
902
908
|
// an application's own etag function may decline
|
|
903
909
|
if (etag) {
|
|
904
910
|
this.headers["etag"] = etag;
|
|
@@ -924,11 +930,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
924
930
|
// Transfer-Encoding the application set, as express 5.3
|
|
925
931
|
if (this.req.method === "HEAD") {
|
|
926
932
|
if (this.statusCode !== 204 && this.statusCode !== 304 && !this.headers["transfer-encoding"]) {
|
|
927
|
-
this.headers["content-length"] = String(Buffer.byteLength(body));
|
|
933
|
+
this.headers["content-length"] = String(Buffer.byteLength(/** @type {string|Buffer} */ (body)));
|
|
928
934
|
}
|
|
929
935
|
return this.end();
|
|
930
936
|
}
|
|
931
|
-
return this.end(body);
|
|
937
|
+
return this.end(/** @type {string|Buffer} */ (body));
|
|
932
938
|
}
|
|
933
939
|
|
|
934
940
|
/**
|
|
@@ -1195,6 +1201,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1195
1201
|
});
|
|
1196
1202
|
} else {
|
|
1197
1203
|
// large files and ranges are piped
|
|
1204
|
+
/** @type {{highWaterMark: number, start?: number, end?: number}} */
|
|
1198
1205
|
const opts = {
|
|
1199
1206
|
highWaterMark: HIGH_WATERMARK
|
|
1200
1207
|
};
|
|
@@ -1258,6 +1265,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1258
1265
|
}
|
|
1259
1266
|
|
|
1260
1267
|
// a header option of sendFile, as Express does: it only goes out once the stat succeeded
|
|
1268
|
+
/** @type {Record<string, string>} */
|
|
1261
1269
|
const headers = {
|
|
1262
1270
|
"Content-Disposition": contentDisposition(name || path)
|
|
1263
1271
|
};
|
|
@@ -1550,8 +1558,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
1550
1558
|
*/
|
|
1551
1559
|
set(field, value) {
|
|
1552
1560
|
if (typeof field === "object") {
|
|
1553
|
-
|
|
1554
|
-
|
|
1561
|
+
const fields = /** @type {Record<string, string|string[]>} */ (field);
|
|
1562
|
+
for (const header in fields) {
|
|
1563
|
+
this.set(header, fields[header]);
|
|
1555
1564
|
}
|
|
1556
1565
|
} else {
|
|
1557
1566
|
const name = field.toLowerCase();
|
|
@@ -1893,8 +1902,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
1893
1902
|
url = status;
|
|
1894
1903
|
status = 302;
|
|
1895
1904
|
}
|
|
1905
|
+
// read from the closures below too, where the checker does not see the shuffle above
|
|
1906
|
+
const code = /** @type {number} */ (status);
|
|
1896
1907
|
this.location(/** @type {string} */ (url));
|
|
1897
|
-
this.status(
|
|
1908
|
+
this.status(code);
|
|
1898
1909
|
|
|
1899
1910
|
const address = /** @type {string} */ (this.get("Location"));
|
|
1900
1911
|
let body;
|
|
@@ -1917,11 +1928,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1917
1928
|
this.format({
|
|
1918
1929
|
text: () => {
|
|
1919
1930
|
this.set("Content-Type", "text/plain; charset=utf-8");
|
|
1920
|
-
body = `${statuses.message[
|
|
1931
|
+
body = `${statuses.message[code]}. Redirecting to ${address}`;
|
|
1921
1932
|
},
|
|
1922
1933
|
html: () => {
|
|
1923
1934
|
this.set("Content-Type", "text/html; charset=utf-8");
|
|
1924
|
-
body = `<p>${statuses.message[
|
|
1935
|
+
body = `<p>${statuses.message[code]}. Redirecting to ${escapeHtml(address)}</p>`;
|
|
1925
1936
|
},
|
|
1926
1937
|
default: () => {
|
|
1927
1938
|
// no Content-Type, as Express leaves it when the client accepts neither
|
package/src/route.js
CHANGED
|
@@ -166,12 +166,14 @@ function checkRouteHandlers(method, handlers) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// every verb, plus all(), registered the same way: flattened, checked, then pushed with the verb
|
|
169
|
-
// they answer. all() pushes handlers with no verb at all, which matches every request
|
|
169
|
+
// they answer. all() pushes handlers with no verb at all, which matches every request. Loose:
|
|
170
|
+
// the members are written here by name and not declared on the class
|
|
171
|
+
const prototype = /** @type {any} */ (Route.prototype);
|
|
170
172
|
for (const method of ["all", ...METHODS.map((verb) => verb.toLowerCase())]) {
|
|
171
|
-
if (method !== "all" && typeof
|
|
173
|
+
if (method !== "all" && typeof prototype[method] === "function") {
|
|
172
174
|
continue;
|
|
173
175
|
}
|
|
174
|
-
|
|
176
|
+
prototype[method] = function (/** @type {unknown[]} */ ...handlers) {
|
|
175
177
|
const flattened = handlers.flat(Infinity);
|
|
176
178
|
checkRouteHandlers(method, flattened);
|
|
177
179
|
// the check above threw on anything else
|
package/src/router-utils.js
CHANGED
|
@@ -53,7 +53,7 @@ const { METHODS } = require("http");
|
|
|
53
53
|
*/
|
|
54
54
|
/**
|
|
55
55
|
* An earlier registration a mount is guarded by, see guardsInside.
|
|
56
|
-
* @typedef {{path: string, use: boolean, method: string, all: boolean}} MountGuard
|
|
56
|
+
* @typedef {{path: string, use: boolean, method: string, all: boolean, params: boolean}} MountGuard
|
|
57
57
|
*/
|
|
58
58
|
/**
|
|
59
59
|
* A layer as express shapes it, for app.stack and router.stack.
|
|
@@ -118,8 +118,19 @@ function anyGuardHits(guards, path) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// every method the declarative compiler can emit: a patched one disables compilation
|
|
121
|
+
/** @type {Record<string, string>} */
|
|
121
122
|
const resCodes = {},
|
|
122
|
-
resDecMethods =
|
|
123
|
+
resDecMethods = /** @type {const} */ ([
|
|
124
|
+
"set",
|
|
125
|
+
"setHeader",
|
|
126
|
+
"header",
|
|
127
|
+
"send",
|
|
128
|
+
"end",
|
|
129
|
+
"append",
|
|
130
|
+
"status",
|
|
131
|
+
"json",
|
|
132
|
+
"sendStatus"
|
|
133
|
+
]);
|
|
123
134
|
for (const method of resDecMethods) {
|
|
124
135
|
resCodes[method] = Response.prototype[method].toString();
|
|
125
136
|
}
|
|
@@ -143,7 +154,7 @@ function layerFor(route, callback) {
|
|
|
143
154
|
: callback.name || "<anonymous>",
|
|
144
155
|
params: undefined,
|
|
145
156
|
path: undefined,
|
|
146
|
-
keys: [],
|
|
157
|
+
keys: /** @type {never[]} */ ([]),
|
|
147
158
|
route: undefined
|
|
148
159
|
};
|
|
149
160
|
route._layers.set(callback, layer);
|
|
@@ -685,6 +696,22 @@ function shadowsLeaf(guard, leafPath, leaf) {
|
|
|
685
696
|
return pathsCanOverlap(guard.path.toLowerCase(), leafPath.toLowerCase(), guard.use);
|
|
686
697
|
}
|
|
687
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Whether a HEAD of this leaf's path could enter a route of another verb written before the mount,
|
|
701
|
+
* for its param() callbacks: Express exempts HEAD from the method check. The GET leaf keeps its
|
|
702
|
+
* registration, only its HEAD twin goes to the generic walk. See headEnters in optimizer.js.
|
|
703
|
+
*
|
|
704
|
+
* @param {MountGuard} guard
|
|
705
|
+
* @param {string} leafPath the leaf's absolute path, parameters and all
|
|
706
|
+
* @returns {boolean}
|
|
707
|
+
*/
|
|
708
|
+
function headEntersGuard(guard, leafPath) {
|
|
709
|
+
if (guard.all || guard.use || !guard.params || guard.method === "GET" || guard.method === "HEAD") {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
return pathsCanOverlap(guard.path.toLowerCase(), leafPath.toLowerCase(), false);
|
|
713
|
+
}
|
|
714
|
+
|
|
688
715
|
/**
|
|
689
716
|
* The layers before a mount that answer some of what is inside it and not all of it, which neither
|
|
690
717
|
* the chain nor uWS's specificity can say. Carried down the walk and asked about every leaf, see
|
|
@@ -714,7 +741,14 @@ function guardsInside(router, mount, pathPrefix, chain, inherited) {
|
|
|
714
741
|
if (guards === inherited) {
|
|
715
742
|
guards = [...inherited];
|
|
716
743
|
}
|
|
717
|
-
guards.push({
|
|
744
|
+
guards.push({
|
|
745
|
+
path: pathPrefix + r.path,
|
|
746
|
+
use: r.use === true,
|
|
747
|
+
method: r.method,
|
|
748
|
+
all: r.all === true,
|
|
749
|
+
// whether its router has param() callbacks, which a HEAD runs on any matching verb
|
|
750
|
+
params: r.paramCallbacks.size > 0
|
|
751
|
+
});
|
|
718
752
|
}
|
|
719
753
|
return guards;
|
|
720
754
|
}
|
|
@@ -798,7 +832,7 @@ function callablePrototypeFor(classPrototype) {
|
|
|
798
832
|
return prototype;
|
|
799
833
|
}
|
|
800
834
|
prototype = Object.create(classPrototype);
|
|
801
|
-
for (const name of ["apply", "call", "toString"]) {
|
|
835
|
+
for (const name of /** @type {const} */ (["apply", "call", "toString"])) {
|
|
802
836
|
Object.defineProperty(prototype, name, {
|
|
803
837
|
value: Function.prototype[name],
|
|
804
838
|
writable: true,
|
|
@@ -870,6 +904,7 @@ module.exports = {
|
|
|
870
904
|
stepsOver,
|
|
871
905
|
couldAnswer,
|
|
872
906
|
shadowsLeaf,
|
|
907
|
+
headEntersGuard,
|
|
873
908
|
guardsInside,
|
|
874
909
|
rememberApp,
|
|
875
910
|
restoreApp,
|
package/src/router.js
CHANGED
|
@@ -74,6 +74,11 @@ const {
|
|
|
74
74
|
/** @typedef {import("./router-utils.js").WsRoute} WsRoute */
|
|
75
75
|
/** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
|
|
76
76
|
/** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
|
|
77
|
+
/**
|
|
78
|
+
* One app.param() call as _runParamCallbacks records it: the value its callbacks were given, the
|
|
79
|
+
* one they left in req.params, and what they ended with.
|
|
80
|
+
* @typedef {{error: unknown, match: unknown, value: unknown}} ParamCall
|
|
81
|
+
*/
|
|
77
82
|
|
|
78
83
|
// one number per app.route(), so the routes it creates know they belong together
|
|
79
84
|
let routeGroups = 0;
|
|
@@ -238,6 +243,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
238
243
|
*/
|
|
239
244
|
_asCallable() {
|
|
240
245
|
const fn = /** @type {any} */ (
|
|
246
|
+
/**
|
|
247
|
+
* @param {any} req loose as handle() is
|
|
248
|
+
* @param {any} res
|
|
249
|
+
* @param {(err?: unknown) => void} [next]
|
|
250
|
+
*/
|
|
241
251
|
function (req, res, next) {
|
|
242
252
|
return fn.handle(req, res, next);
|
|
243
253
|
}
|
|
@@ -668,6 +678,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
668
678
|
// a RegExp mount matches a piece of path known only per request
|
|
669
679
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
670
680
|
userRegexp: path instanceof RegExp,
|
|
681
|
+
// a compiled pattern that cannot be compared as text, decided below
|
|
682
|
+
complex: false,
|
|
671
683
|
methods: methodMap,
|
|
672
684
|
stack,
|
|
673
685
|
// the route as a request sees it, a view with the written path when normalised
|
|
@@ -1052,6 +1064,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1052
1064
|
let name = "";
|
|
1053
1065
|
/** @type {unknown} */
|
|
1054
1066
|
let value;
|
|
1067
|
+
/** @type {ParamCall} */
|
|
1055
1068
|
let entry;
|
|
1056
1069
|
/** @type {Function[]} */
|
|
1057
1070
|
let fns = [];
|
|
@@ -1074,10 +1087,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1074
1087
|
}
|
|
1075
1088
|
name = names[index++];
|
|
1076
1089
|
value = req.params[name];
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
req.params[name] =
|
|
1080
|
-
return nextParam(
|
|
1090
|
+
const earlier = called.get(name);
|
|
1091
|
+
if (earlier && (earlier.match === value || (earlier.error && earlier.error !== "route"))) {
|
|
1092
|
+
req.params[name] = earlier.value;
|
|
1093
|
+
return nextParam(earlier.error);
|
|
1081
1094
|
}
|
|
1082
1095
|
entry = { error: null, match: value, value };
|
|
1083
1096
|
called.set(name, entry);
|
|
@@ -1298,7 +1311,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1298
1311
|
* @param {Record<string, any>} [carried] the headers the error asked for, written last
|
|
1299
1312
|
*/
|
|
1300
1313
|
_sendErrorPage(request, response, err, checkEnv = false, carried = undefined) {
|
|
1301
|
-
|
|
1314
|
+
const page = this._generateErrorPage(err, response.statusCode, checkEnv);
|
|
1302
1315
|
request.noEtag = true;
|
|
1303
1316
|
// a header that cannot be written may be what brought the request here
|
|
1304
1317
|
response._dropUnwritableHeaders();
|
|
@@ -1312,7 +1325,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1312
1325
|
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1313
1326
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1314
1327
|
response.setHeader("Content-Security-Policy", "default-src 'none'");
|
|
1315
|
-
response.send(
|
|
1328
|
+
response.send(page);
|
|
1316
1329
|
}
|
|
1317
1330
|
|
|
1318
1331
|
/**
|
|
@@ -1368,10 +1381,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
1368
1381
|
useRouterClass(module.exports);
|
|
1369
1382
|
|
|
1370
1383
|
// the verb methods on the prototype: as own arrows they closed over the instance they were built
|
|
1371
|
-
// on, and express.Router().post(...) answered with the object the callable was copied from
|
|
1372
|
-
|
|
1384
|
+
// on, and express.Router().post(...) answered with the object the callable was copied from.
|
|
1385
|
+
// Loose: the members are written here by name and not declared on the class
|
|
1386
|
+
const prototype = /** @type {any} */ (module.exports.prototype);
|
|
1373
1387
|
for (const method of methods) {
|
|
1374
|
-
|
|
1388
|
+
prototype[method] = function (
|
|
1375
1389
|
/** @type {string|RegExp|(string|RegExp)[]} */ path,
|
|
1376
1390
|
/** @type {unknown[]} */ ...callbacks
|
|
1377
1391
|
) {
|