fulmine.js 5.21.3 → 5.21.5
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 +62 -13
- package/src/nest.js +2 -0
- package/src/optimizer.js +65 -7
- package/src/options.d.ts +1 -1
- package/src/request.js +25 -18
- package/src/response-utils.js +65 -5
- package/src/response.js +185 -69
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.21.
|
|
3
|
+
"version": "5.21.5",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js, up to 20x faster. Same API, your middleware and framework keep working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"test": "node tests/index.js",
|
|
23
|
+
"gates": "node tools/gates.js",
|
|
23
24
|
"test:unit": "node --expose-gc --test --test-timeout=120000 \"tests/unit/*.test.js\"",
|
|
24
25
|
"test:types": "tsd --files tests/types/*.test-d.ts",
|
|
25
26
|
"test:express": "node tools/express-suite.js",
|
|
@@ -137,10 +138,13 @@
|
|
|
137
138
|
"@release-it/conventional-changelog": "^12.0.0",
|
|
138
139
|
"@types/accepts": "^1.3.7",
|
|
139
140
|
"@types/bytes": "^3.1.5",
|
|
141
|
+
"@types/compressible": "^2.0.3",
|
|
140
142
|
"@types/content-disposition": "^0.5.9",
|
|
143
|
+
"@types/cookie": "^0.6.0",
|
|
141
144
|
"@types/cookie-signature": "^1.1.2",
|
|
142
145
|
"@types/encodeurl": "^1.0.3",
|
|
143
146
|
"@types/etag": "^1.8.4",
|
|
147
|
+
"@types/fast-decode-uri-component": "^1.0.0",
|
|
144
148
|
"@types/fresh": "^0.5.3",
|
|
145
149
|
"@types/mime-types": "^3.0.1",
|
|
146
150
|
"@types/ms": "^2.1.0",
|
package/src/adopt.js
CHANGED
|
@@ -36,7 +36,10 @@ const UWS_SPEC = /** @type {string} */ (require("../package.json").dependencies[
|
|
|
36
36
|
/** The pnpm override that drops this package's own copy, so the project's direct one is the only one. */
|
|
37
37
|
const UWS_OVERRIDE = `${SELF}>${UWS}`;
|
|
38
38
|
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Where each manager keeps its substitutions, and what to call it when telling someone.
|
|
41
|
+
* @type {Record<string, {keys: string[], reinstall: string}>}
|
|
42
|
+
*/
|
|
40
43
|
const MANAGERS = {
|
|
41
44
|
npm: { keys: ["overrides"], reinstall: "npm install" },
|
|
42
45
|
pnpm: { keys: ["pnpm", "overrides"], reinstall: "pnpm install" },
|
package/src/application.js
CHANGED
|
@@ -41,6 +41,9 @@ const { registerWebSocketRoutes } = require("./websocket.js");
|
|
|
41
41
|
const { addServerMembers } = require("./server-shape.js");
|
|
42
42
|
const { workerCount, forkWorkers, isSupervising, becomeSupervisor } = require("./cluster.js");
|
|
43
43
|
|
|
44
|
+
/** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
|
|
45
|
+
/** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
|
|
46
|
+
|
|
44
47
|
const cpuCount = os.cpus().length;
|
|
45
48
|
|
|
46
49
|
// marks a "trust proxy" the application never set, under express's key, so a sub-app may inherit
|
|
@@ -95,12 +98,14 @@ class Application extends Router {
|
|
|
95
98
|
/**
|
|
96
99
|
* A mounted app's settings chain onto its parent's, as in express.
|
|
97
100
|
*
|
|
101
|
+
* @override
|
|
98
102
|
* @type {boolean}
|
|
99
103
|
*/
|
|
100
104
|
_inheritsSettings = true;
|
|
101
105
|
|
|
102
106
|
/**
|
|
103
107
|
* See Router#_isApplication.
|
|
108
|
+
* @override
|
|
104
109
|
* @type {boolean}
|
|
105
110
|
*/
|
|
106
111
|
_isApplication = true;
|
|
@@ -141,7 +146,7 @@ class Application extends Router {
|
|
|
141
146
|
/** @type {Response} */
|
|
142
147
|
response;
|
|
143
148
|
|
|
144
|
-
/** @type {boolean} */
|
|
149
|
+
/** @override @type {boolean} */
|
|
145
150
|
listenCalled = false;
|
|
146
151
|
|
|
147
152
|
/** @type {FSWorker[]} */
|
|
@@ -487,6 +492,7 @@ class Application extends Router {
|
|
|
487
492
|
* @param {import("./router-utils.js").NativePreset} [preset] see nativePreset
|
|
488
493
|
* @param {import("./router-utils.js").SkipHolder} [skipHolder] forwarded whole, dropping it
|
|
489
494
|
* silently turned every skip off
|
|
495
|
+
* @override
|
|
490
496
|
* @returns {Request} the request, with the response as request.res
|
|
491
497
|
*/
|
|
492
498
|
handleRequest(res, req, preset, skipHolder) {
|
|
@@ -509,6 +515,7 @@ class Application extends Router {
|
|
|
509
515
|
* The µWS app, for what µWS offers that this does not (socket.io attaches to it). Made on
|
|
510
516
|
* first ask, so an app served through node's http never loads the binary, see src/uws.js.
|
|
511
517
|
*
|
|
518
|
+
* @override
|
|
512
519
|
* @returns {any}
|
|
513
520
|
*/
|
|
514
521
|
get uwsApp() {
|
|
@@ -521,7 +528,9 @@ class Application extends Router {
|
|
|
521
528
|
|
|
522
529
|
/** The catch-all uWS handler, for every request no native route took. */
|
|
523
530
|
_createRequestHandler() {
|
|
524
|
-
this.uwsApp.any("/*", (
|
|
531
|
+
this.uwsApp.any("/*", (/** @type {UwsResponse} */ res, /** @type {UwsRequest} */ req) =>
|
|
532
|
+
this._serveGeneric(res, req)
|
|
533
|
+
);
|
|
525
534
|
}
|
|
526
535
|
|
|
527
536
|
/**
|
package/src/cli.js
CHANGED
|
@@ -41,6 +41,7 @@ const TO = "fulmine.js";
|
|
|
41
41
|
|
|
42
42
|
// modules this has a faster version of, reported and not rewritten: the replacement lives on the
|
|
43
43
|
// express import, which may not be in scope where these are required
|
|
44
|
+
/** @type {Record<string, string>} */
|
|
44
45
|
const BUILT_IN_INSTEAD = {
|
|
45
46
|
compression: "express.compression(), which takes the same options",
|
|
46
47
|
"serve-static": "express.static()",
|
package/src/compression.js
CHANGED
|
@@ -20,6 +20,12 @@ limitations under the License.
|
|
|
20
20
|
* What res.on was given, parked until there is a compressor to hang it on.
|
|
21
21
|
* @typedef {Parameters<import("stream").Writable["on"]>} OnArgs
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* What the hooks below take: writeHead's arguments in node's two shapes, see applyWriteHead, and
|
|
25
|
+
* the callback write() and end() may carry in place of the encoding.
|
|
26
|
+
* @typedef {Parameters<Response["writeHead"]>} WriteHeadArgs
|
|
27
|
+
* @typedef {(err?: Error|null) => void} WriteCallback
|
|
28
|
+
*/
|
|
23
29
|
|
|
24
30
|
// express.compression(): the compression module's options, defaults and decision order, with three
|
|
25
31
|
// differences: a whole body (res.send, res.json) is compressed in one call and goes out with a
|
|
@@ -229,8 +235,10 @@ function toBuffer(chunk, encoding) {
|
|
|
229
235
|
* compressed at all. The default says yes to any compressible content type.
|
|
230
236
|
* @param {string} [options.enforceEncoding] what to use when the request carries no
|
|
231
237
|
* Accept-Encoding at all. Default "identity", which is to say nothing is compressed.
|
|
232
|
-
* @param {
|
|
233
|
-
*
|
|
238
|
+
* @param {import("zlib").BrotliOptions} [options.brotli] brotli options, `params` included. The
|
|
239
|
+
* default quality is 4.
|
|
240
|
+
* @param {import("zlib").ZstdOptions} [options.zstd] zstd options, `params` included, node's own
|
|
241
|
+
* defaults otherwise.
|
|
234
242
|
* @param {string[]} [options.encodings] the encodings this middleware may answer with, out of
|
|
235
243
|
* "br", "zstd", "gzip" and "deflate". What is not named is never used, however the client ranks
|
|
236
244
|
* it. An uncompressed answer is always on offer, and enforceEncoding is outside this list. This
|
|
@@ -251,7 +259,7 @@ function compression(options) {
|
|
|
251
259
|
const brotliOptions = { ...opts.brotli };
|
|
252
260
|
brotliOptions.params = {
|
|
253
261
|
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
254
|
-
...(opts.brotli &&
|
|
262
|
+
...(opts.brotli && opts.brotli.params)
|
|
255
263
|
};
|
|
256
264
|
// zstd at node's default level is already in the band this middleware wants
|
|
257
265
|
const zstdOptions = { ...opts.zstd };
|
|
@@ -380,11 +388,21 @@ function compression(options) {
|
|
|
380
388
|
}
|
|
381
389
|
};
|
|
382
390
|
// at writeHead too, its headers applied first as on-headers orders it
|
|
391
|
+
/**
|
|
392
|
+
* @param {WriteHeadArgs[0]} statusCode
|
|
393
|
+
* @param {WriteHeadArgs[1]} [statusMessage]
|
|
394
|
+
* @param {WriteHeadArgs[2]} [headers]
|
|
395
|
+
*/
|
|
383
396
|
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
384
397
|
const reason = applyWriteHead(this, statusMessage, headers);
|
|
385
398
|
vary();
|
|
386
399
|
return _plainWriteHead.call(this, statusCode, reason);
|
|
387
400
|
};
|
|
401
|
+
/**
|
|
402
|
+
* @param {any} [chunk]
|
|
403
|
+
* @param {BufferEncoding|WriteCallback} [encoding]
|
|
404
|
+
* @param {WriteCallback} [callback]
|
|
405
|
+
*/
|
|
388
406
|
res.end = function end(chunk, encoding, callback) {
|
|
389
407
|
vary();
|
|
390
408
|
return _plainEnd.call(this, chunk, encoding, callback);
|
|
@@ -492,6 +510,11 @@ function compression(options) {
|
|
|
492
510
|
}
|
|
493
511
|
|
|
494
512
|
// decided at writeHead too, its headers applied first as on-headers orders it
|
|
513
|
+
/**
|
|
514
|
+
* @param {WriteHeadArgs[0]} statusCode
|
|
515
|
+
* @param {WriteHeadArgs[1]} [statusMessage]
|
|
516
|
+
* @param {WriteHeadArgs[2]} [headers]
|
|
517
|
+
*/
|
|
495
518
|
res.writeHead = function writeHead(statusCode, statusMessage, headers) {
|
|
496
519
|
const reason = applyWriteHead(this, statusMessage, headers);
|
|
497
520
|
if (!decided) {
|
|
@@ -500,6 +523,11 @@ function compression(options) {
|
|
|
500
523
|
return _writeHead.call(this, statusCode, reason);
|
|
501
524
|
};
|
|
502
525
|
|
|
526
|
+
/**
|
|
527
|
+
* @param {any} chunk
|
|
528
|
+
* @param {BufferEncoding|WriteCallback} [encoding]
|
|
529
|
+
* @param {WriteCallback} [callback]
|
|
530
|
+
*/
|
|
503
531
|
res.write = function write(chunk, encoding, callback) {
|
|
504
532
|
if (typeof encoding === "function") {
|
|
505
533
|
callback = encoding;
|
|
@@ -520,6 +548,11 @@ function compression(options) {
|
|
|
520
548
|
return _write.call(this, chunk, encoding, callback);
|
|
521
549
|
};
|
|
522
550
|
|
|
551
|
+
/**
|
|
552
|
+
* @param {any} [chunk]
|
|
553
|
+
* @param {BufferEncoding|WriteCallback} [encoding]
|
|
554
|
+
* @param {WriteCallback} [callback]
|
|
555
|
+
*/
|
|
523
556
|
res.end = function end(chunk, encoding, callback) {
|
|
524
557
|
if (typeof chunk === "function") {
|
|
525
558
|
callback = chunk;
|
|
@@ -575,6 +608,10 @@ function compression(options) {
|
|
|
575
608
|
return _end.call(this, chunk, callback);
|
|
576
609
|
};
|
|
577
610
|
|
|
611
|
+
/**
|
|
612
|
+
* @param {OnArgs[0]} type
|
|
613
|
+
* @param {OnArgs[1]} listener
|
|
614
|
+
*/
|
|
578
615
|
res.on = function on(type, listener) {
|
|
579
616
|
if (!listeners || type !== "drain") {
|
|
580
617
|
return _on.call(this, type, listener);
|
package/src/declarative.js
CHANGED
|
@@ -144,6 +144,7 @@ function literalValue(node) {
|
|
|
144
144
|
return literalValue(element);
|
|
145
145
|
});
|
|
146
146
|
case "ObjectExpression": {
|
|
147
|
+
/** @type {Record<string, unknown>} */
|
|
147
148
|
const out = {};
|
|
148
149
|
for (const property of node.properties) {
|
|
149
150
|
if (property.type !== "Property" || property.computed || property.kind !== "init") {
|
|
@@ -537,7 +538,8 @@ function readHandler(cb) {
|
|
|
537
538
|
}
|
|
538
539
|
}
|
|
539
540
|
|
|
540
|
-
|
|
541
|
+
// undefined for a destructured one, which readParamNames reads by hand
|
|
542
|
+
const args = fn.params.map((/** @type {any} */ param) => param.name);
|
|
541
543
|
|
|
542
544
|
if (args.length < 2) {
|
|
543
545
|
return null;
|
|
@@ -607,12 +609,14 @@ function readBindings(pattern, into) {
|
|
|
607
609
|
function readParamNames(fn, args) {
|
|
608
610
|
const [req, res] = args;
|
|
609
611
|
let queryName, paramsName;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
+
/** @type {Binding[]} */
|
|
613
|
+
const queries = [];
|
|
614
|
+
/** @type {Binding[]} */
|
|
615
|
+
const params = [];
|
|
612
616
|
|
|
613
617
|
if (fn.params[0].type === "ObjectPattern") {
|
|
614
|
-
const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
|
|
615
|
-
const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
|
|
618
|
+
const query = fn.params[0].properties.find((/** @type {any} */ prop) => prop.key.name === "query");
|
|
619
|
+
const param = fn.params[0].properties.find((/** @type {any} */ prop) => prop.key.name === "params");
|
|
616
620
|
|
|
617
621
|
if (query?.value?.type === "Identifier") {
|
|
618
622
|
queryName = query.value.name;
|
package/src/lazy-readable.js
CHANGED
|
@@ -71,21 +71,21 @@ for (const member of [
|
|
|
71
71
|
} else if (descriptor.get || descriptor.set) {
|
|
72
72
|
const innerGet = descriptor.get;
|
|
73
73
|
const innerSet = descriptor.set;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
// the original's accessor pair, each half wrapped where there is one
|
|
75
|
+
const wrapped = { ...descriptor };
|
|
76
|
+
if (innerGet) {
|
|
77
|
+
wrapped.get = /** @this {import("stream").Readable} */ function () {
|
|
78
|
+
materialise(this);
|
|
79
|
+
return innerGet.call(this);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (innerSet) {
|
|
83
|
+
wrapped.set = /** @this {import("stream").Readable} @param {unknown} value */ function (value) {
|
|
84
|
+
materialise(this);
|
|
85
|
+
innerSet.call(this, value);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
Object.defineProperty(LazyReadableBase.prototype, member, wrapped);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
package/src/lazy-writable.js
CHANGED
|
@@ -65,21 +65,21 @@ for (const member of [
|
|
|
65
65
|
} else if (descriptor.get || descriptor.set) {
|
|
66
66
|
const innerGet = descriptor.get;
|
|
67
67
|
const innerSet = descriptor.set;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
// the original's accessor pair, each half wrapped where there is one
|
|
69
|
+
const wrapped = { ...descriptor };
|
|
70
|
+
if (innerGet) {
|
|
71
|
+
wrapped.get = /** @this {import("stream").Writable} */ function () {
|
|
72
|
+
materialiseWritable(this);
|
|
73
|
+
return innerGet.call(this);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (innerSet) {
|
|
77
|
+
wrapped.set = /** @this {import("stream").Writable} @param {unknown} value */ function (value) {
|
|
78
|
+
materialiseWritable(this);
|
|
79
|
+
innerSet.call(this, value);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
Object.defineProperty(LazyWritableBase.prototype, member, wrapped);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
package/src/middlewares.js
CHANGED
|
@@ -31,13 +31,43 @@ const parseQuery = require("./parse-query.js");
|
|
|
31
31
|
const { kGetSafe } = require("./usage.js");
|
|
32
32
|
const { AsyncResource } = require("async_hooks");
|
|
33
33
|
|
|
34
|
+
// node's unexported Symbol("context_frame"), where an AsyncResource keeps the store it captured,
|
|
35
|
+
// read off the first resource. null when node has none, then every binding keeps its scope
|
|
36
|
+
/** @type {symbol|null|undefined} */
|
|
37
|
+
let kContextFrame;
|
|
38
|
+
// set once an init hook was seen, never cleared: from then on every binding has its scope as before
|
|
39
|
+
let initHooksSeen = false;
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
|
-
* AsyncResource.bind without node's generic wrapper
|
|
42
|
+
* raw-body's wrap: AsyncResource.bind without node's generic wrapper (~1.9us per call there), and no
|
|
43
|
+
* scope without an AsyncLocalStorage store or an init hook: 2.3 to 3.5us per request in a uWS
|
|
44
|
+
* callback, +9 to 16% on the arena's POST. new AsyncResource("") throws only while a hook listens.
|
|
36
45
|
*
|
|
37
46
|
* @param {(...args: any[]) => any} fn called with at most one argument by every caller here
|
|
38
47
|
* @returns {(err?: any) => any}
|
|
39
48
|
*/
|
|
40
49
|
function bindContext(fn) {
|
|
50
|
+
if (!initHooksSeen) {
|
|
51
|
+
/** @type {AsyncResource|undefined} */
|
|
52
|
+
let probe;
|
|
53
|
+
try {
|
|
54
|
+
probe = new AsyncResource("");
|
|
55
|
+
} catch {
|
|
56
|
+
initHooksSeen = true;
|
|
57
|
+
}
|
|
58
|
+
if (probe !== undefined) {
|
|
59
|
+
if (kContextFrame === undefined) {
|
|
60
|
+
kContextFrame =
|
|
61
|
+
Object.getOwnPropertySymbols(probe).find((s) => s.description === "context_frame") ?? null;
|
|
62
|
+
}
|
|
63
|
+
if (kContextFrame !== null && /** @type {any} */ (probe)[kContextFrame] === undefined) {
|
|
64
|
+
return fn;
|
|
65
|
+
}
|
|
66
|
+
// a store to carry, and no init hook to have seen the empty type: the probe is the resource
|
|
67
|
+
const resource = probe;
|
|
68
|
+
return (err) => resource.runInAsyncScope(fn, undefined, err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
41
71
|
const resource = new AsyncResource(fn.name || "bound-anonymous-fn");
|
|
42
72
|
return (err) => resource.runInAsyncScope(fn, undefined, err);
|
|
43
73
|
}
|
|
@@ -586,8 +616,14 @@ function serveStatic(root, options) {
|
|
|
586
616
|
}
|
|
587
617
|
const ext = path.extname(fullpath);
|
|
588
618
|
let i = 0;
|
|
589
|
-
// no extension on a directory; on the decoded url, as send
|
|
590
|
-
|
|
619
|
+
// no extension on a directory; on the decoded url and only after ENOENT, as send does:
|
|
620
|
+
// ENOTDIR or ENAMETOOLONG report the path asked, not the last extension (fuzz seed 3561875562)
|
|
621
|
+
if (
|
|
622
|
+
/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT" &&
|
|
623
|
+
ext === "" &&
|
|
624
|
+
!url.endsWith("/") &&
|
|
625
|
+
options.extensions
|
|
626
|
+
) {
|
|
591
627
|
while (i < options.extensions.length) {
|
|
592
628
|
try {
|
|
593
629
|
stat = fs.statSync(fullpath + "." + options.extensions[i]);
|
|
@@ -720,7 +756,7 @@ function inflateError(inflate, err) {
|
|
|
720
756
|
*
|
|
721
757
|
* @param {string|undefined} rawContentEncoding
|
|
722
758
|
* @param {any} options the parser's options, read loosely: only inflate is looked at
|
|
723
|
-
* @returns {{inflate?: Inflater, error?: HttpError}}
|
|
759
|
+
* @returns {{inflate?: Inflater|undefined, error?: HttpError}}
|
|
724
760
|
*/
|
|
725
761
|
function encodingFor(rawContentEncoding, options) {
|
|
726
762
|
if (!options.inflate) {
|
|
@@ -841,6 +877,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
841
877
|
/** @type {string[]|null|undefined} the "body methods" setting, read on the first request */
|
|
842
878
|
let additionalMethods;
|
|
843
879
|
|
|
880
|
+
/**
|
|
881
|
+
* @param {Request & {body?: unknown}} req
|
|
882
|
+
* @param {Response} res
|
|
883
|
+
* @param {(err?: unknown) => void} next
|
|
884
|
+
*/
|
|
844
885
|
const parserMiddleware = (req, res, next) => {
|
|
845
886
|
// the prologue is synchronous, bindContext waits for the read (1.4us of nothing here)
|
|
846
887
|
|
|
@@ -872,8 +913,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
872
913
|
}
|
|
873
914
|
|
|
874
915
|
if (options.simpleType) {
|
|
875
|
-
|
|
876
|
-
|
|
916
|
+
// only a type function lets a request without a content-type past the check above,
|
|
917
|
+
// and simpleType is never set beside one
|
|
918
|
+
const header = /** @type {string} */ (type);
|
|
919
|
+
const semicolonIndex = header.indexOf(";");
|
|
920
|
+
const clearType = semicolonIndex !== -1 ? header.substring(0, semicolonIndex) : header;
|
|
877
921
|
// the trim and lowercase only when the exact compare fails
|
|
878
922
|
if (clearType !== options.simpleType && clearType.trim().toLowerCase() !== options.simpleType) {
|
|
879
923
|
return next();
|
|
@@ -884,7 +928,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
884
928
|
return next();
|
|
885
929
|
}
|
|
886
930
|
} else {
|
|
887
|
-
if (!claimsType(type)) {
|
|
931
|
+
if (!claimsType(/** @type {string} */ (type))) {
|
|
888
932
|
return next();
|
|
889
933
|
}
|
|
890
934
|
}
|
|
@@ -1107,11 +1151,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1107
1151
|
}
|
|
1108
1152
|
}
|
|
1109
1153
|
// fewer bytes than content-length promised; not when inflating, it counts the compressed ones
|
|
1110
|
-
if (!inflate &&
|
|
1154
|
+
if (!inflate && !Number.isNaN(lengthNumber) && totalSize !== lengthNumber) {
|
|
1111
1155
|
return next(
|
|
1112
1156
|
bodyError("request size did not match content length", 400, "request.size.invalid", {
|
|
1113
|
-
expected:
|
|
1114
|
-
length:
|
|
1157
|
+
expected: lengthNumber,
|
|
1158
|
+
length: lengthNumber,
|
|
1115
1159
|
received: totalSize
|
|
1116
1160
|
})
|
|
1117
1161
|
);
|
|
@@ -1166,9 +1210,14 @@ const json = createBodyParser(
|
|
|
1166
1210
|
|
|
1167
1211
|
// strict: only an object or an array is a body
|
|
1168
1212
|
if (options.strict !== false) {
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1213
|
+
// the four characters body-parser skips, space, tab, LF and CR, with a loop: the regex
|
|
1214
|
+
// allocated a match array per body, 56ns to 14
|
|
1215
|
+
let at = 0;
|
|
1216
|
+
let code = text.charCodeAt(0);
|
|
1217
|
+
while (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
|
|
1218
|
+
code = text.charCodeAt(++at);
|
|
1219
|
+
}
|
|
1220
|
+
const first = at < text.length ? text[at] : undefined;
|
|
1172
1221
|
if (first !== "{" && first !== "[") {
|
|
1173
1222
|
// a SyntaxError, as body-parser builds it
|
|
1174
1223
|
return next(
|
package/src/nest.js
CHANGED
|
@@ -56,6 +56,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
|
|
|
56
56
|
/**
|
|
57
57
|
* The app is the server. Nest calls this once, from NestApplication's constructor.
|
|
58
58
|
*
|
|
59
|
+
* @override
|
|
59
60
|
* @param {import("@nestjs/common").NestApplicationOptions} [options] the options NestFactory.create was given
|
|
60
61
|
* @returns {void}
|
|
61
62
|
*/
|
|
@@ -84,6 +85,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
|
|
|
84
85
|
* for them in `app.router.stack`; there is no such array here, so a second call was putting a
|
|
85
86
|
* second pair in front of every request.
|
|
86
87
|
*
|
|
88
|
+
* @override
|
|
87
89
|
* @param {string} [prefix]
|
|
88
90
|
* @param {boolean} [rawBody]
|
|
89
91
|
* @returns {void}
|
package/src/optimizer.js
CHANGED
|
@@ -20,6 +20,8 @@ limitations under the License.
|
|
|
20
20
|
/** @typedef {import("./router.js")} Router */
|
|
21
21
|
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
22
22
|
/** @typedef {import("./application.js").Application} Application */
|
|
23
|
+
/** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
|
|
24
|
+
/** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
|
|
23
25
|
|
|
24
26
|
const {
|
|
25
27
|
patternToRegex,
|
|
@@ -44,6 +46,7 @@ const {
|
|
|
44
46
|
hasErrorMiddleware,
|
|
45
47
|
couldAnswer,
|
|
46
48
|
shadowsLeaf,
|
|
49
|
+
headEntersGuard,
|
|
47
50
|
guardsInside,
|
|
48
51
|
supportedUwsMethods,
|
|
49
52
|
regExParam
|
|
@@ -79,6 +82,8 @@ function optimizeRoute(router, route, routes) {
|
|
|
79
82
|
const strictHere = (route.owner ?? router)._strictRouting();
|
|
80
83
|
/** @type {string[]|null} earlier literals a case variant could smuggle a request past */
|
|
81
84
|
let caseGuards = null;
|
|
85
|
+
// whether the HEAD twin has to stay generic, see headEnters
|
|
86
|
+
let headGeneric = false;
|
|
82
87
|
|
|
83
88
|
for (let i = 0; i < routes.length; i++) {
|
|
84
89
|
const r = routes[i];
|
|
@@ -95,6 +100,12 @@ function optimizeRoute(router, route, routes) {
|
|
|
95
100
|
if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
96
101
|
return false;
|
|
97
102
|
}
|
|
103
|
+
// Express exempts HEAD from the method check: a HEAD enters a matching route of
|
|
104
|
+
// any verb and its param() callbacks run, as the generic walk does. The chain
|
|
105
|
+
// cannot say whether such a route matches, so the HEAD twin stays generic
|
|
106
|
+
if (route.method === "GET" && headEnters(r, route, caseSensitive, strictHere)) {
|
|
107
|
+
headGeneric = true;
|
|
108
|
+
}
|
|
98
109
|
continue;
|
|
99
110
|
}
|
|
100
111
|
}
|
|
@@ -115,7 +126,7 @@ function optimizeRoute(router, route, routes) {
|
|
|
115
126
|
route.path.slice(0, matched[0].length) === matched[0] &&
|
|
116
127
|
(route.path.length === matched[0].length || route.path[matched[0].length] === "/");
|
|
117
128
|
if (runsAlways) {
|
|
118
|
-
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
129
|
+
if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
|
|
119
130
|
return false;
|
|
120
131
|
}
|
|
121
132
|
optimizedPath.push(r);
|
|
@@ -137,7 +148,7 @@ function optimizeRoute(router, route, routes) {
|
|
|
137
148
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
138
149
|
r.pattern === "/*"))
|
|
139
150
|
) {
|
|
140
|
-
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
151
|
+
if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
|
|
141
152
|
return false;
|
|
142
153
|
}
|
|
143
154
|
optimizedPath.push(r);
|
|
@@ -164,7 +175,7 @@ function optimizeRoute(router, route, routes) {
|
|
|
164
175
|
}
|
|
165
176
|
// the same path lands on the same µWS registration, so the earlier route runs from the chain
|
|
166
177
|
if (rPathFolded === routePathFolded) {
|
|
167
|
-
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
178
|
+
if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
|
|
168
179
|
return false;
|
|
169
180
|
}
|
|
170
181
|
optimizedPath.push(r);
|
|
@@ -186,10 +197,45 @@ function optimizeRoute(router, route, routes) {
|
|
|
186
197
|
}
|
|
187
198
|
optimizedPath.push(route);
|
|
188
199
|
route._caseGuards = caseGuards;
|
|
200
|
+
route._headGeneric = headGeneric;
|
|
189
201
|
|
|
190
202
|
return optimizedPath;
|
|
191
203
|
}
|
|
192
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Whether a HEAD of some path this GET route answers would enter the earlier route of another
|
|
207
|
+
* verb for its param() callbacks: the router has callbacks and the paths can meet. A wrong yes
|
|
208
|
+
* costs the HEAD twin its native registration and nothing else.
|
|
209
|
+
*
|
|
210
|
+
* @param {RouteEntry} r the earlier route, of another verb
|
|
211
|
+
* @param {RouteEntry} route the GET route
|
|
212
|
+
* @param {boolean} caseSensitive
|
|
213
|
+
* @param {boolean} strictHere
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
function headEnters(r, route, caseSensitive, strictHere) {
|
|
217
|
+
if (r.paramCallbacks.size === 0 || typeof route.path !== "string") {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (!route.path.includes(":")) {
|
|
221
|
+
if (typeof r.pattern === "string") {
|
|
222
|
+
return (
|
|
223
|
+
r.pattern === "/*" ||
|
|
224
|
+
(caseSensitive ? r.pattern === route.path : r.patternLower === route.path.toLowerCase())
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/"));
|
|
228
|
+
}
|
|
229
|
+
if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return pathsCanOverlap(
|
|
233
|
+
caseSensitive ? r.path : r.path.toLowerCase(),
|
|
234
|
+
caseSensitive ? route.path : route.path.toLowerCase(),
|
|
235
|
+
r.use
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
193
239
|
/**
|
|
194
240
|
* Hands every route reachable by path alone to the native uWS router, walking into mounted
|
|
195
241
|
* routers with their prefix. Runs once, at listen.
|
|
@@ -284,6 +330,16 @@ function compileOptimizedRoutes(root) {
|
|
|
284
330
|
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
285
331
|
continue;
|
|
286
332
|
}
|
|
333
|
+
// a route of another verb before the mount that a HEAD would enter, see headEnters
|
|
334
|
+
if (
|
|
335
|
+
!route._headGeneric &&
|
|
336
|
+
route.method === "GET" &&
|
|
337
|
+
outerGuards.length > 0 &&
|
|
338
|
+
typeof route.path === "string" &&
|
|
339
|
+
outerGuards.some((g) => headEntersGuard(g, pathPrefix + route.path))
|
|
340
|
+
) {
|
|
341
|
+
route._headGeneric = true;
|
|
342
|
+
}
|
|
287
343
|
// an earlier parameter route in the same router would take this literal path
|
|
288
344
|
if (leafPath.length > 1) {
|
|
289
345
|
const shadow = leafPath[leafPath.length - 2];
|
|
@@ -360,6 +416,7 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
360
416
|
* @param {import("./router-utils.js").NativePreset|undefined} preset
|
|
361
417
|
* @param {{skipHeaders: boolean, skipQuery: boolean}} skips
|
|
362
418
|
* @param {string|null} wireMethod
|
|
419
|
+
* @returns {(res: UwsResponse, req: UwsRequest) => unknown} what µWS calls
|
|
363
420
|
*/
|
|
364
421
|
const makeHandler = (chain, preset, skips, wireMethod) => {
|
|
365
422
|
// where a granted skip lives, so a middleware added after listen can take it back: the
|
|
@@ -395,10 +452,11 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
395
452
|
return router._refuseRequest(response);
|
|
396
453
|
}
|
|
397
454
|
if (optimizedParams) {
|
|
398
|
-
// slicing them out of the path instead measured a wash
|
|
455
|
+
// slicing them out of the path instead measured a wash. The index is one the
|
|
456
|
+
// registered path declares, so the value is never undefined
|
|
399
457
|
request.optimizedParams = new NullObject();
|
|
400
458
|
for (let i = 0; i < optimizedParams.length; i++) {
|
|
401
|
-
request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
|
|
459
|
+
request.optimizedParams[optimizedParams[i]] = /** @type {string} */ (req.getParameter(i));
|
|
402
460
|
}
|
|
403
461
|
}
|
|
404
462
|
const walk = new Walk(router, request, response, chain, true, skipUntil, nativeDone, nativeFail);
|
|
@@ -513,7 +571,7 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
513
571
|
? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips, wireMethod)
|
|
514
572
|
: fn;
|
|
515
573
|
router.uwsApp[method](replacedPath + "/", slashFn);
|
|
516
|
-
if (method === "get") {
|
|
574
|
+
if (method === "get" && !route._headGeneric) {
|
|
517
575
|
router.uwsApp.head(
|
|
518
576
|
replacedPath + "/",
|
|
519
577
|
makeHandler(
|
|
@@ -525,7 +583,7 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
525
583
|
);
|
|
526
584
|
}
|
|
527
585
|
}
|
|
528
|
-
if (method === "get") {
|
|
586
|
+
if (method === "get" && !route._headGeneric) {
|
|
529
587
|
// its own handler, the shared one would carry GET
|
|
530
588
|
router.uwsApp.head(
|
|
531
589
|
replacedPath,
|
package/src/options.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface SendFileOptions {
|
|
|
43
43
|
/** Extra headers for the response. */
|
|
44
44
|
headers?: Record<string, string>;
|
|
45
45
|
/** Internal: express.static's setHeaders, which res.sendFile itself does not take. */
|
|
46
|
-
_setHeaders?: (res: any, path: string, stat: any) => void;
|
|
46
|
+
_setHeaders?: ((res: any, path: string, stat: any) => void) | undefined;
|
|
47
47
|
/** First byte of the window to send. */
|
|
48
48
|
start?: number;
|
|
49
49
|
/** Last byte of the window to send. */
|