fulmine.js 5.19.8 → 5.20.0
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 +115 -985
- package/package.json +16 -4
- package/src/adopt.js +116 -5
- package/src/application.js +8 -4
- package/src/cli.js +26 -3
- package/src/compression.js +1 -0
- package/src/create.js +248 -0
- package/src/declarative.js +15 -3
- package/src/hot-settings.js +7 -0
- package/src/index.js +23 -0
- package/src/middlewares.js +6 -2
- package/src/node-shim.js +34 -10
- package/src/optimizer.js +24 -3
- package/src/request-utils.js +3 -2
- package/src/request.js +8 -15
- package/src/response-utils.js +1 -1
- package/src/response.js +15 -1
- package/src/route.js +12 -6
- package/src/router-utils.js +11 -2
- package/src/router.js +27 -4
- package/src/testing.js +1 -1
- package/src/utils.js +28 -9
- package/src/verify.js +108 -7
- package/src/walk.js +8 -3
- package/src/websocket.js +1 -1
package/src/hot-settings.js
CHANGED
|
@@ -27,14 +27,21 @@ class HotSettings {
|
|
|
27
27
|
constructor() {
|
|
28
28
|
this.epoch = 0;
|
|
29
29
|
this.xPoweredBy = false;
|
|
30
|
+
/** @type {((body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string)|undefined} */
|
|
30
31
|
this.etagFn = undefined;
|
|
31
32
|
// null means every method, which is express's behaviour and the default
|
|
33
|
+
/** @type {Set<string>|null} */
|
|
32
34
|
this.etagMethods = null;
|
|
35
|
+
/** @type {((query: string|null) => Record<string, any>)|undefined} */
|
|
33
36
|
this.queryParserFn = undefined;
|
|
37
|
+
/** @type {import("./utils.js").TrustFn|undefined} */
|
|
34
38
|
this.trustProxyFn = undefined;
|
|
35
39
|
this.trustProxyProtocol = false;
|
|
40
|
+
/** @type {boolean|undefined} */
|
|
36
41
|
this.jsonEscape = undefined;
|
|
42
|
+
/** @type {any} the "json replacer" setting, as stringify takes it */
|
|
37
43
|
this.jsonReplacer = undefined;
|
|
44
|
+
/** @type {string|number|undefined} */
|
|
38
45
|
this.jsonSpaces = undefined;
|
|
39
46
|
}
|
|
40
47
|
}
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,28 @@ limitations under the License.
|
|
|
21
21
|
// package ships, so the module is read through a loose alias
|
|
22
22
|
const uWS = require("uWebSockets.js");
|
|
23
23
|
const uWSAny = /** @type {any} */ (uWS);
|
|
24
|
+
|
|
25
|
+
// A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
|
|
26
|
+
// one installed can drift from the one this package pins and was tested against. Said once, at
|
|
27
|
+
// require time, where it reaches every deployment rather than only the ones that run verify.
|
|
28
|
+
{
|
|
29
|
+
const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
|
|
30
|
+
/** @type {string|undefined} */
|
|
31
|
+
let installed;
|
|
32
|
+
try {
|
|
33
|
+
// its exports map does not expose package.json, so it is read beside the entry point
|
|
34
|
+
const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
|
|
35
|
+
installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
|
|
36
|
+
} catch {
|
|
37
|
+
// nothing to compare against, which is not worth a warning of its own
|
|
38
|
+
}
|
|
39
|
+
if (pinned && installed && installed !== pinned) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
|
|
42
|
+
" On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
24
46
|
const Application = require("./application.js");
|
|
25
47
|
const Router = require("./router.js");
|
|
26
48
|
const Route = require("./route.js");
|
|
@@ -68,6 +90,7 @@ try {
|
|
|
68
90
|
module.exports = /** @type {any} */ (Application);
|
|
69
91
|
|
|
70
92
|
// a router is a function too: it has to be callable to be used as middleware
|
|
93
|
+
/** @param {object} [options] the options express.Router() takes */
|
|
71
94
|
module.exports.Router = function (options) {
|
|
72
95
|
return new Router(options)._asCallable();
|
|
73
96
|
};
|
package/src/middlewares.js
CHANGED
|
@@ -139,6 +139,7 @@ function stripBom(text) {
|
|
|
139
139
|
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** @type {typeof import("iconv-lite")|undefined} */
|
|
142
143
|
let iconv;
|
|
143
144
|
|
|
144
145
|
/**
|
|
@@ -966,6 +967,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
966
967
|
(contentType) => !!typeis.is(contentType, /** @type {string[]} */ (options.type))
|
|
967
968
|
);
|
|
968
969
|
|
|
970
|
+
/** @type {string[]|null|undefined} the "body methods" setting, read on the first request */
|
|
969
971
|
let additionalMethods;
|
|
970
972
|
|
|
971
973
|
const parserMiddleware = (req, res, next) => {
|
|
@@ -1038,6 +1040,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1038
1040
|
// halves sit on either side of the encoding, which is the order body-parser reads
|
|
1039
1041
|
// them in: the charset this parser accepts at all, then the Content-Encoding, then
|
|
1040
1042
|
// whether iconv knows the charset.
|
|
1043
|
+
/** @type {string|undefined} */
|
|
1041
1044
|
let encoding;
|
|
1042
1045
|
if (charsetPolicy) {
|
|
1043
1046
|
encoding = charsetOf(type) ?? defaultCharset;
|
|
@@ -1129,7 +1132,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1129
1132
|
// not us, and would wait for an end that is never coming
|
|
1130
1133
|
req.complete = true;
|
|
1131
1134
|
req.readable = false;
|
|
1132
|
-
req._res.collectBody(limit, (body) => {
|
|
1135
|
+
req._res.collectBody(limit, (/** @type {ArrayBuffer|null} */ body) => {
|
|
1133
1136
|
if (body === null) {
|
|
1134
1137
|
// over maxSize: uWS refused it natively
|
|
1135
1138
|
return next(
|
|
@@ -1165,6 +1168,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1165
1168
|
// known and we are not inflating, the final size is known up front, so chunks go
|
|
1166
1169
|
// straight into one buffer and the body is copied once. The cap means a client that
|
|
1167
1170
|
// declares a body and never sends it costs no more than one that sends it
|
|
1171
|
+
/** @type {Buffer[]} */
|
|
1168
1172
|
const abs = [];
|
|
1169
1173
|
const declaredLength = inflate ? -1 : Number(length);
|
|
1170
1174
|
let target =
|
|
@@ -1310,7 +1314,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1310
1314
|
// if we are fast enough (not async), we can do it
|
|
1311
1315
|
// otherwise we need to use a stream since it already started streaming it
|
|
1312
1316
|
if (!req.receivedData) {
|
|
1313
|
-
req._res.onData((ab, isLast) => {
|
|
1317
|
+
req._res.onData((/** @type {ArrayBuffer} */ ab, /** @type {boolean} */ isLast) => {
|
|
1314
1318
|
onData(ab);
|
|
1315
1319
|
if (isLast) {
|
|
1316
1320
|
// this subscription replaced the Readable's own, so the stream will
|
package/src/node-shim.js
CHANGED
|
@@ -39,21 +39,30 @@ function addressToBytes(address) {
|
|
|
39
39
|
if (!address) {
|
|
40
40
|
return new ArrayBuffer(0);
|
|
41
41
|
}
|
|
42
|
-
// node reports an IPv4 client on a dual stack socket as ::ffff:127.0.0.1
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// node reports an IPv4 client on a dual stack socket as ::ffff:127.0.0.1 and one on an IPv4
|
|
43
|
+
// socket as 127.0.0.1, which is the difference req.ip reads back out of the width: uWS hands
|
|
44
|
+
// the mapped peer over as the sixteen bytes, the plain one as four. Keeping node's own form
|
|
45
|
+
// rather than flattening both to four is what makes the shim answer what node answers,
|
|
46
|
+
// whichever socket accepted the connection
|
|
47
|
+
const mapped = address.startsWith("::ffff:") && address.includes(".");
|
|
48
|
+
const dotted = mapped ? address.slice(7) : address;
|
|
49
|
+
if (dotted.includes(".")) {
|
|
50
|
+
const parts = dotted.split(".");
|
|
47
51
|
if (parts.length !== 4) {
|
|
48
52
|
return new ArrayBuffer(0);
|
|
49
53
|
}
|
|
50
|
-
const bytes = new Uint8Array(4);
|
|
54
|
+
const bytes = new Uint8Array(mapped ? 16 : 4);
|
|
55
|
+
const offset = mapped ? 12 : 0;
|
|
56
|
+
if (mapped) {
|
|
57
|
+
bytes[10] = 0xff;
|
|
58
|
+
bytes[11] = 0xff;
|
|
59
|
+
}
|
|
51
60
|
for (let i = 0; i < 4; i++) {
|
|
52
61
|
const value = Number(parts[i]);
|
|
53
62
|
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
54
63
|
return new ArrayBuffer(0);
|
|
55
64
|
}
|
|
56
|
-
bytes[i] = value;
|
|
65
|
+
bytes[offset + i] = value;
|
|
57
66
|
}
|
|
58
67
|
return bytes.buffer;
|
|
59
68
|
}
|
|
@@ -80,13 +89,19 @@ function addressToBytes(address) {
|
|
|
80
89
|
return view.buffer;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* A chunk as the ArrayBuffer uWS deals in, copied rather than viewed so nothing aliases node's.
|
|
94
|
+
*
|
|
95
|
+
* @param {ArrayBuffer|Buffer|string} chunk
|
|
96
|
+
* @returns {ArrayBuffer}
|
|
97
|
+
*/
|
|
84
98
|
function toArrayBuffer(chunk) {
|
|
85
99
|
if (chunk instanceof ArrayBuffer) {
|
|
86
100
|
return chunk;
|
|
87
101
|
}
|
|
88
102
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
89
|
-
|
|
103
|
+
// node types the store as possibly shared, but a Buffer it made or was given here never is
|
|
104
|
+
return /** @type {ArrayBuffer} */ (buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
/**
|
|
@@ -175,6 +190,7 @@ class NodeHttpResponse {
|
|
|
175
190
|
// how much of the body has gone out, which is what uWS reports through getWriteOffset and
|
|
176
191
|
// hands back to an onWritable callback
|
|
177
192
|
this._offset = 0;
|
|
193
|
+
/** @type {((offset: number) => boolean)|null} */
|
|
178
194
|
this._onWritable = null;
|
|
179
195
|
this._aborted = false;
|
|
180
196
|
// the current body handler: uWS keeps one and a second onData replaces it, so this does too
|
|
@@ -195,6 +211,8 @@ class NodeHttpResponse {
|
|
|
195
211
|
* uWS batches everything written inside this into one syscall. node has no equivalent that
|
|
196
212
|
* means the same thing, and its own cork would hold the write until the callback returned
|
|
197
213
|
* without changing what is sent, so this only runs it.
|
|
214
|
+
*
|
|
215
|
+
* @param {() => void} cb
|
|
198
216
|
*/
|
|
199
217
|
cork(cb) {
|
|
200
218
|
cb();
|
|
@@ -300,13 +318,19 @@ class NodeHttpResponse {
|
|
|
300
318
|
* Called when there is room to write again. uWS asks the handler to answer whether it managed
|
|
301
319
|
* to write everything, and calls it again if not; node's drain says nothing, so the handler is
|
|
302
320
|
* kept until the next drain and its answer ignored.
|
|
321
|
+
*
|
|
322
|
+
* @param {(offset: number) => boolean} handler
|
|
303
323
|
*/
|
|
304
324
|
onWritable(handler) {
|
|
305
325
|
this._onWritable = handler;
|
|
306
326
|
return this;
|
|
307
327
|
}
|
|
308
328
|
|
|
309
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* Called when the connection goes before the response is finished.
|
|
331
|
+
*
|
|
332
|
+
* @param {() => void} handler
|
|
333
|
+
*/
|
|
310
334
|
onAborted(handler) {
|
|
311
335
|
this._nodeRes.on("close", () => {
|
|
312
336
|
if (!this._nodeRes.writableFinished) {
|
package/src/optimizer.js
CHANGED
|
@@ -51,6 +51,7 @@ const {
|
|
|
51
51
|
|
|
52
52
|
// router.js requires this file while it is still being evaluated, so its class cannot be required
|
|
53
53
|
// from here: it hands it over on the line under its own export instead.
|
|
54
|
+
/** @type {typeof import("./router.js")} */
|
|
54
55
|
let Router;
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -233,6 +234,12 @@ function compileOptimizedRoutes(root) {
|
|
|
233
234
|
|
|
234
235
|
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
235
236
|
// carries what was written before them and answers only part of what is under them
|
|
237
|
+
/**
|
|
238
|
+
* @param {Router} router
|
|
239
|
+
* @param {string} pathPrefix
|
|
240
|
+
* @param {RouteEntry[]} chainPrefix
|
|
241
|
+
* @param {import("./router-utils.js").MountGuard[]} outerGuards
|
|
242
|
+
*/
|
|
236
243
|
const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
|
|
237
244
|
for (const route of router._routes) {
|
|
238
245
|
if (route.use) {
|
|
@@ -344,7 +351,7 @@ function compileOptimizedRoutes(root) {
|
|
|
344
351
|
if (route._caseGuards) {
|
|
345
352
|
// compared against the whole path µWS matched, so they carry the mount
|
|
346
353
|
// prefix, folded along with the rest of it
|
|
347
|
-
registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
|
|
354
|
+
registered._caseGuards = route._caseGuards.map((/** @type {string} */ p) => pathPrefix + p);
|
|
348
355
|
}
|
|
349
356
|
root._registerUwsRoute(registered, chain);
|
|
350
357
|
// the chain holds the original object, so the request-time guard has to find
|
|
@@ -388,18 +395,27 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
388
395
|
method = "del";
|
|
389
396
|
}
|
|
390
397
|
if (route.path.includes(":")) {
|
|
391
|
-
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
398
|
+
route.optimizedParams = route.path.match(regExParam).map((/** @type {string} */ p) => p.slice(1));
|
|
392
399
|
}
|
|
393
400
|
// null for almost every route: only a parameter route with an earlier literal that a case
|
|
394
401
|
// variant could slip past carries one, see _optimizeRoute. Built once here, and matched
|
|
395
402
|
// insensitively, since that is the folding the guard exists for
|
|
396
403
|
const caseGuards = route._caseGuards
|
|
397
|
-
? route._caseGuards.map((
|
|
404
|
+
? route._caseGuards.map((/** @type {string} */ p) =>
|
|
405
|
+
needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
|
|
406
|
+
)
|
|
398
407
|
: null;
|
|
408
|
+
/**
|
|
409
|
+
* @param {RouteEntry[]} chain
|
|
410
|
+
* @param {import("./router-utils.js").NativePreset|undefined} preset
|
|
411
|
+
* @param {{skipHeaders: boolean, skipQuery: boolean}} skips
|
|
412
|
+
* @param {string|null} wireMethod
|
|
413
|
+
*/
|
|
399
414
|
const makeHandler = (chain, preset, skips, wireMethod) => {
|
|
400
415
|
// the mutable object a granted skip lives on, so a middleware arriving after listen can
|
|
401
416
|
// take it back: a literal registration's preset doubles as it, a parameterised one gets a
|
|
402
417
|
// holder of its own. It carries the method too, so the constructor settles it in one compare
|
|
418
|
+
/** @type {import("./router-utils.js").SkipHolder|undefined} */
|
|
403
419
|
let skipHolder = preset;
|
|
404
420
|
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
|
|
405
421
|
skipHolder = {
|
|
@@ -504,6 +520,11 @@ function registerUwsRoute(router, route, optimizedPath) {
|
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
522
|
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
523
|
+
/**
|
|
524
|
+
* @param {string} path
|
|
525
|
+
* @param {string} method
|
|
526
|
+
* @param {{skipHeaders: boolean, skipQuery: boolean}} skips
|
|
527
|
+
*/
|
|
507
528
|
const makePreset = (path, method, skips) => {
|
|
508
529
|
const preset = nativePreset(path, method);
|
|
509
530
|
if (skips.skipHeaders || skips.skipQuery) {
|
package/src/request-utils.js
CHANGED
|
@@ -104,8 +104,9 @@ function isMappedIPv4(bytes) {
|
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps it
|
|
107
|
-
* whenever the listener is dual stack, which is every listen() without an IPv4 address.
|
|
108
|
-
*
|
|
107
|
+
* whenever the listener is dual stack, which is every listen() without an IPv4 address. Only the
|
|
108
|
+
* address invented after the response has ended asks: every address actually read says which form
|
|
109
|
+
* it is in by its width, uWS handing a mapped peer over as sixteen bytes and a plain one as four.
|
|
109
110
|
*
|
|
110
111
|
* @param {import("./application.js").Application} app the application the request arrived at
|
|
111
112
|
* @returns {boolean}
|
package/src/request.js
CHANGED
|
@@ -204,13 +204,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
204
204
|
*/
|
|
205
205
|
rawIp;
|
|
206
206
|
|
|
207
|
-
/**
|
|
208
|
-
* Whether rawIp came from a PROXY protocol preamble rather than from the socket. Only the
|
|
209
|
-
* IPv4 mapping reads it, see parsedIp. Declared for the same reason as rawIp.
|
|
210
|
-
* @type {boolean}
|
|
211
|
-
*/
|
|
212
|
-
_ipFromProxy = false;
|
|
213
|
-
|
|
214
207
|
/**
|
|
215
208
|
* Whether the request declared a body, content-length or transfer-encoding, spotted during
|
|
216
209
|
* the header copy. Declared for the same reason as rawIp.
|
|
@@ -457,7 +450,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
457
450
|
this._lastMethod = this.method;
|
|
458
451
|
// the folded _opPath and the percent scan of _originalPath, built on the hop that first
|
|
459
452
|
// wants them and dropped by every rewrite, see _pathMatches and Walk#dispatch
|
|
453
|
+
/** @type {string|null} */
|
|
460
454
|
this._opPathLower = null;
|
|
455
|
+
/** @type {boolean|null} */
|
|
461
456
|
this._mayFailDecode = null;
|
|
462
457
|
this.params = {};
|
|
463
458
|
|
|
@@ -478,6 +473,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
478
473
|
this._paramStack = null;
|
|
479
474
|
// route and application in pairs, one pair per mounted application entered from another
|
|
480
475
|
// application, so handing back puts the one that was current back, see rememberApp
|
|
476
|
+
/** @type {any[]|undefined} route and app alternating, loose because the pairs share one array */
|
|
481
477
|
this._appStack = undefined;
|
|
482
478
|
this.receivedData = false;
|
|
483
479
|
// node's IncomingMessage flag: false until the whole body has arrived. on-finished
|
|
@@ -1048,7 +1044,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
1048
1044
|
const proxied = uwsRes.getProxiedRemoteAddress();
|
|
1049
1045
|
// empty unless a preamble arrived, which is the only thing that tells the two apart
|
|
1050
1046
|
if (proxied.byteLength !== 0) {
|
|
1051
|
-
this._ipFromProxy = true;
|
|
1052
1047
|
return proxied;
|
|
1053
1048
|
}
|
|
1054
1049
|
}
|
|
@@ -1084,14 +1079,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
1084
1079
|
/** @type {string|undefined} */
|
|
1085
1080
|
let ip;
|
|
1086
1081
|
if (rawIp.byteLength === 4) {
|
|
1087
|
-
// ipv4
|
|
1082
|
+
// ipv4, and plain: four bytes mean the peer arrived over IPv4 on an IPv4 socket, which
|
|
1083
|
+
// is what node writes plain as well. A dual stack listener hands an IPv4 peer over as
|
|
1084
|
+
// the mapped sixteen below, the node shim passes node's own form through, and an
|
|
1085
|
+
// address a proxy declared is the four numbers the proxy sent, so none of them wants
|
|
1086
|
+
// a prefix invented here
|
|
1088
1087
|
ip = new Uint8Array(rawIp).join(".");
|
|
1089
|
-
// the mapped form belongs to a dual stack listener, which is what makes an IPv4 peer
|
|
1090
|
-
// arrive as ::ffff:a.b.c.d. An address a proxy declared never came through that socket,
|
|
1091
|
-
// so it is left as the four numbers the proxy sent
|
|
1092
|
-
if (!this._ipFromProxy && mapsIPv4Peer(this.app)) {
|
|
1093
|
-
ip = "::ffff:" + ip;
|
|
1094
|
-
}
|
|
1095
1088
|
} else if (rawIp.byteLength === 16) {
|
|
1096
1089
|
const bytes = new Uint8Array(rawIp);
|
|
1097
1090
|
if (isMappedIPv4(bytes)) {
|
package/src/response-utils.js
CHANGED
|
@@ -68,7 +68,7 @@ for (const s of [
|
|
|
68
68
|
// One status line per code, built on first use: the default path, with no custom reason phrase,
|
|
69
69
|
// paid a template string and a trim per request for a line that never changes. Bounded to real
|
|
70
70
|
// HTTP codes so a wild writeHead value cannot grow the array or flip it into dictionary mode.
|
|
71
|
-
const STATUS_LINES = [];
|
|
71
|
+
const STATUS_LINES = /** @type {string[]} */ ([]);
|
|
72
72
|
/**
|
|
73
73
|
* @param {number} code
|
|
74
74
|
* @param {string|undefined} text an explicit reason phrase, which bypasses the cache
|
package/src/response.js
CHANGED
|
@@ -208,6 +208,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
208
208
|
this.req = req;
|
|
209
209
|
this._res = res;
|
|
210
210
|
this.headersSent = false;
|
|
211
|
+
// whether a body goes out, decided from the method that arrived, as node decides it when
|
|
212
|
+
// it builds its ServerResponse: a middleware that rewrites req.method afterwards, which
|
|
213
|
+
// method-override does, changes what the router matches and not what the wire carries
|
|
214
|
+
this._hasBody = req._isHead !== true;
|
|
211
215
|
this.app = app;
|
|
212
216
|
this.locals = new NullObject();
|
|
213
217
|
this.finished = false;
|
|
@@ -238,6 +242,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
238
242
|
this.headers["x-powered-by"] = "Fulmine";
|
|
239
243
|
}
|
|
240
244
|
|
|
245
|
+
/** @type {unknown} a slot for whatever a middleware puts on res.body, never read here */
|
|
241
246
|
this.body = undefined;
|
|
242
247
|
// what was handed to uWS, kept so a caller asking for content-length after the fact can be
|
|
243
248
|
// answered, see get(). Undefined until the response ends, and for one that sends no body
|
|
@@ -811,7 +816,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
811
816
|
} else {
|
|
812
817
|
// a Buffer goes to uWS as the view it is: copying it into a fresh ArrayBuffer was
|
|
813
818
|
// an allocation per body, and uWS reads the view's own offset and length
|
|
814
|
-
if (this.
|
|
819
|
+
if (!this._hasBody) {
|
|
815
820
|
const length = Buffer.byteLength(data ?? "");
|
|
816
821
|
this.headers["content-length"] = String(length);
|
|
817
822
|
this._res.endWithoutBody(length, closeConnection);
|
|
@@ -961,6 +966,15 @@ module.exports = class Response extends LazyWritable {
|
|
|
961
966
|
delete this.headers["transfer-encoding"];
|
|
962
967
|
body = "";
|
|
963
968
|
}
|
|
969
|
+
// express's send reads req.method here and hands node's end() nothing for a HEAD, the
|
|
970
|
+
// length already set, and node frames what it is handed: a GET a middleware made a HEAD
|
|
971
|
+
// answers its length and no body. end() alone decides by the wire, see _hasBody
|
|
972
|
+
if (this.req.method === "HEAD") {
|
|
973
|
+
if (this.statusCode !== 204 && this.statusCode !== 304) {
|
|
974
|
+
this.headers["content-length"] = String(Buffer.byteLength(body));
|
|
975
|
+
}
|
|
976
|
+
return this.end();
|
|
977
|
+
}
|
|
964
978
|
return this.end(body);
|
|
965
979
|
}
|
|
966
980
|
|
package/src/route.js
CHANGED
|
@@ -48,6 +48,10 @@ class Route {
|
|
|
48
48
|
* @returns {boolean}
|
|
49
49
|
*/
|
|
50
50
|
handlesMethod(method) {
|
|
51
|
+
// all() answers every verb, registered or not, which is what express reads _all for
|
|
52
|
+
if (this.methods._all) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
51
55
|
const lowered = method.toLowerCase();
|
|
52
56
|
return Boolean(this.methods[lowered] || (lowered === "head" && this.methods.get));
|
|
53
57
|
}
|
|
@@ -87,6 +91,7 @@ class Route {
|
|
|
87
91
|
}
|
|
88
92
|
req.route = this;
|
|
89
93
|
|
|
94
|
+
/** @param {unknown} [err] */
|
|
90
95
|
const next = (err) => {
|
|
91
96
|
// next("route") leaves this route, and next("router") leaves whoever is running it
|
|
92
97
|
if (err === "route") {
|
|
@@ -129,7 +134,7 @@ class Route {
|
|
|
129
134
|
// does it. A bare rejection carries none and gets the one express invents. Thenable
|
|
130
135
|
// too, which express deprecates but still waits for
|
|
131
136
|
if (out && typeof out.then === "function") {
|
|
132
|
-
out.then(null, (thrown) => next(thrown || new Error("Rejected promise")));
|
|
137
|
+
out.then(null, (/** @type {unknown} */ thrown) => next(thrown || new Error("Rejected promise")));
|
|
133
138
|
}
|
|
134
139
|
} catch (thrown) {
|
|
135
140
|
next(thrown);
|
|
@@ -162,14 +167,15 @@ for (const method of ["all", ...METHODS.map((verb) => verb.toLowerCase())]) {
|
|
|
162
167
|
if (method !== "all" && typeof Route.prototype[method] === "function") {
|
|
163
168
|
continue;
|
|
164
169
|
}
|
|
165
|
-
Route.prototype[method] = function (...handlers) {
|
|
170
|
+
Route.prototype[method] = function (/** @type {unknown[]} */ ...handlers) {
|
|
166
171
|
const flattened = handlers.flat(Infinity);
|
|
167
172
|
checkRouteHandlers(method, flattened);
|
|
168
|
-
|
|
173
|
+
// the check above threw on anything else
|
|
174
|
+
for (const handle of /** @type {Function[]} */ (flattened)) {
|
|
169
175
|
this.stack.push({ method: method === "all" ? undefined : method, handle });
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
// express marks the route _all rather than naming every verb, and whoever reads
|
|
177
|
+
// req.route.methods reads that key, so it is written the same way here
|
|
178
|
+
this.methods[method === "all" ? "_all" : method] = true;
|
|
173
179
|
}
|
|
174
180
|
return this;
|
|
175
181
|
};
|
package/src/router-utils.js
CHANGED
|
@@ -175,8 +175,14 @@ function layerFor(route, callback) {
|
|
|
175
175
|
* @returns {Layer} the layer object, which is express's shape and not one of ours
|
|
176
176
|
*/
|
|
177
177
|
function routeLayer(route) {
|
|
178
|
+
/**
|
|
179
|
+
* @param {Request} req
|
|
180
|
+
* @param {Response} res
|
|
181
|
+
* @param {(err?: unknown) => void} next
|
|
182
|
+
*/
|
|
178
183
|
const handle = function handle(req, res, next) {
|
|
179
184
|
let index = 0;
|
|
185
|
+
/** @param {unknown} [err] */
|
|
180
186
|
const step = (err) => {
|
|
181
187
|
const callback = route.callbacks[index++];
|
|
182
188
|
if (callback === undefined) {
|
|
@@ -206,6 +212,7 @@ function routeLayer(route) {
|
|
|
206
212
|
* The 404 epilogue stays on a microtask, where the await used to resume: a middleware that writes
|
|
207
213
|
* after calling next() must still win the headersSent check, as it does in express.
|
|
208
214
|
* @this {Walk}
|
|
215
|
+
* @param {RouteEntry|false} matched what the walk ended on, as the resolve receives it
|
|
209
216
|
*/
|
|
210
217
|
function nativeDone(matched) {
|
|
211
218
|
if (this.settled) {
|
|
@@ -238,6 +245,7 @@ function nativeDone(matched) {
|
|
|
238
245
|
* an unhandled rejection. Deferred like the resolve, since every rejection used to reach the
|
|
239
246
|
* handler's catch through an await.
|
|
240
247
|
* @this {Walk}
|
|
248
|
+
* @param {unknown} err
|
|
241
249
|
*/
|
|
242
250
|
function nativeFail(err) {
|
|
243
251
|
if (this.settled) {
|
|
@@ -313,7 +321,7 @@ const PATH_PROPERTY = {
|
|
|
313
321
|
const ABSORB_URL = Request.prototype._absorbUrlRewrite;
|
|
314
322
|
const ABSORB_METHOD = Request.prototype._absorbMethodRewrite;
|
|
315
323
|
|
|
316
|
-
const NO_PARAM_NAMES = [];
|
|
324
|
+
const NO_PARAM_NAMES = /** @type {string[]} */ ([]);
|
|
317
325
|
|
|
318
326
|
/**
|
|
319
327
|
* The parameter names a route captures with its own pattern.
|
|
@@ -636,7 +644,8 @@ function hasErrorMiddleware(router) {
|
|
|
636
644
|
}
|
|
637
645
|
|
|
638
646
|
/**
|
|
639
|
-
*
|
|
647
|
+
* @param {unknown[]} handlers what a registration was given, flattened
|
|
648
|
+
* @param {string} [emptyMessage]
|
|
640
649
|
*/
|
|
641
650
|
function checkHandlers(handlers, emptyMessage = "argument handler is required") {
|
|
642
651
|
if (handlers.length === 0) {
|
package/src/router.js
CHANGED
|
@@ -169,6 +169,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
169
169
|
|
|
170
170
|
this._paramCallbacks = new Map();
|
|
171
171
|
this._mountpathCache = new Map();
|
|
172
|
+
/** @type {RouteEntry[]} */
|
|
172
173
|
this._routes = [];
|
|
173
174
|
// websocket routes, kept apart from the HTTP ones: µWS serves them itself and listen()
|
|
174
175
|
// hands them over whole, mount paths and all
|
|
@@ -908,6 +909,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
908
909
|
*/
|
|
909
910
|
_handleError(err, handler, request, response) {
|
|
910
911
|
if (handler) {
|
|
912
|
+
/** @param {unknown} [pass] */
|
|
911
913
|
const next = (pass) => {
|
|
912
914
|
delete request._error;
|
|
913
915
|
delete request._errorKey;
|
|
@@ -1154,15 +1156,20 @@ module.exports = class Router extends EventEmitter {
|
|
|
1154
1156
|
return new Promise((resolve) => {
|
|
1155
1157
|
let index = 0;
|
|
1156
1158
|
let name = "";
|
|
1159
|
+
/** @type {unknown} */
|
|
1157
1160
|
let value;
|
|
1158
1161
|
let entry;
|
|
1162
|
+
/** @type {Function[]} */
|
|
1159
1163
|
let fns = [];
|
|
1160
1164
|
let fnIndex = 0;
|
|
1161
1165
|
|
|
1162
1166
|
// one parameter after the other, err being what the last one's callbacks ended with
|
|
1167
|
+
/** @param {unknown} [err] */
|
|
1163
1168
|
const nextParam = (err) => {
|
|
1164
1169
|
if (err) {
|
|
1165
|
-
|
|
1170
|
+
// an error already in flight stays the one in flight: express reaches a mount's
|
|
1171
|
+
// param callbacks with it pending and goes on with next(layerError || err)
|
|
1172
|
+
if (err !== "route" && !req._error) {
|
|
1166
1173
|
req._error = err;
|
|
1167
1174
|
req._errorKey = route.routeKey;
|
|
1168
1175
|
req._errorGroup = route.group;
|
|
@@ -1188,6 +1195,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1188
1195
|
};
|
|
1189
1196
|
|
|
1190
1197
|
// and one callback of the current parameter after the other
|
|
1198
|
+
/** @param {unknown} [err] */
|
|
1191
1199
|
const nextCallback = (err) => {
|
|
1192
1200
|
const fn = fns[fnIndex++];
|
|
1193
1201
|
// read before the callback runs and again after it: one that rewrites
|
|
@@ -1248,6 +1256,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1248
1256
|
|
|
1249
1257
|
/**
|
|
1250
1258
|
* Resolves with the route that answered, or false when nothing matched.
|
|
1259
|
+
*
|
|
1260
|
+
* @param {Request} req
|
|
1261
|
+
* @param {Response} res
|
|
1262
|
+
* @param {number} [startIndex]
|
|
1263
|
+
* @param {RouteEntry[]} [routes]
|
|
1264
|
+
* @param {boolean} [skipCheck] take the route at the index without matching it, see Walk
|
|
1265
|
+
* @param {RouteEntry} [skipUntil] route to resume after when this chain runs out, see Walk
|
|
1251
1266
|
* @returns {Promise<RouteEntry|false>}
|
|
1252
1267
|
*/
|
|
1253
1268
|
_routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
@@ -1366,11 +1381,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1366
1381
|
// one map for the whole chain, because express builds one Route for it: a request answered
|
|
1367
1382
|
// by the get() of an app.route() reads post() in its req.route.methods too
|
|
1368
1383
|
const groupMethods = new NullObject();
|
|
1384
|
+
/** @type {(Omit<Layer, "route"> & {method: string|undefined})[]} express's Route#stack, see createRoute */
|
|
1369
1385
|
const groupStack = [];
|
|
1370
1386
|
// express hands back a Route, which carries these three beside the verb methods
|
|
1371
1387
|
fns.path = path;
|
|
1372
1388
|
fns.methods = groupMethods;
|
|
1373
1389
|
fns.stack = groupStack;
|
|
1390
|
+
/**
|
|
1391
|
+
* @param {string} method
|
|
1392
|
+
* @param {unknown[]} callbacks
|
|
1393
|
+
*/
|
|
1374
1394
|
const inGroup = (method, callbacks) => {
|
|
1375
1395
|
this._pendingGroup = group;
|
|
1376
1396
|
this._pendingGroupMethods = groupMethods;
|
|
@@ -1384,9 +1404,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1384
1404
|
}
|
|
1385
1405
|
};
|
|
1386
1406
|
for (const method of methods) {
|
|
1387
|
-
fns[method] = (...callbacks) => inGroup(method, callbacks);
|
|
1407
|
+
fns[method] = (/** @type {unknown[]} */ ...callbacks) => inGroup(method, callbacks);
|
|
1388
1408
|
}
|
|
1389
|
-
fns.get = (...callbacks) => inGroup("GET", callbacks);
|
|
1409
|
+
fns.get = (/** @type {unknown[]} */ ...callbacks) => inGroup("GET", callbacks);
|
|
1390
1410
|
return fns;
|
|
1391
1411
|
}
|
|
1392
1412
|
|
|
@@ -1482,7 +1502,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1482
1502
|
useRouterClass(module.exports);
|
|
1483
1503
|
|
|
1484
1504
|
for (const method of methods) {
|
|
1485
|
-
module.exports.prototype[method] = function (
|
|
1505
|
+
module.exports.prototype[method] = function (
|
|
1506
|
+
/** @type {string|RegExp|(string|RegExp)[]} */ path,
|
|
1507
|
+
/** @type {unknown[]} */ ...callbacks
|
|
1508
|
+
) {
|
|
1486
1509
|
return this.createRoute(method, path, this, ...callbacks);
|
|
1487
1510
|
};
|
|
1488
1511
|
}
|
package/src/testing.js
CHANGED
|
@@ -134,7 +134,7 @@ function select(app, patterns, caller) {
|
|
|
134
134
|
throw new TypeError(`${caller} needs a path, or a list of them, to check`);
|
|
135
135
|
}
|
|
136
136
|
const report = routeReport(app);
|
|
137
|
-
const selected = [];
|
|
137
|
+
const selected = /** @type {ReturnType<typeof routeReport>} */ ([]);
|
|
138
138
|
for (const pattern of wanted) {
|
|
139
139
|
const matched = report.filter((entry) => names(entry, pattern));
|
|
140
140
|
if (matched.length === 0) {
|