fulmine.js 5.15.0 → 5.16.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 +11 -0
- package/package.json +1 -1
- package/src/compression.js +46 -3
- package/src/middlewares.js +69 -45
- package/src/parse-query.js +29 -1
- package/src/request.js +151 -57
- package/src/response.js +56 -13
- package/src/router.js +304 -78
- package/src/types.d.ts +6 -0
- package/src/utils.js +16 -0
package/src/router.js
CHANGED
|
@@ -242,36 +242,45 @@ class Walk {
|
|
|
242
242
|
// frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
|
|
243
243
|
// calling this per route measured 0.45us of a scan of four hundred
|
|
244
244
|
router._freezeRoutingFlags();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
r
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
245
|
+
if (routes === router._routes) {
|
|
246
|
+
// the router's own table has an index over its literal routes, so the scan visits
|
|
247
|
+
// the handful that could match instead of every one, see _scanFrom
|
|
248
|
+
routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
|
|
249
|
+
} else {
|
|
250
|
+
// a compiled chain's own array, always short: the linear scan stays.
|
|
251
|
+
// Written out rather than through a predicate handed to findIndexStartingFrom,
|
|
252
|
+
// which was one closure per hop of every request not on a compiled chain
|
|
253
|
+
const method = req.method;
|
|
254
|
+
const length = routes.length;
|
|
255
|
+
for (; routeIndex < length; routeIndex++) {
|
|
256
|
+
const r = routes[routeIndex];
|
|
257
|
+
// A HEAD request enters a route whose path matched even when its verb cannot
|
|
258
|
+
// serve one: express exempts HEAD from the method check ("if (!hasMethod &&
|
|
259
|
+
// method !== 'HEAD')" in router/index.js), so the layer's parameters are
|
|
260
|
+
// captured and its param() callbacks run before the route is dropped. Only
|
|
261
|
+
// asked when the router has callbacks to run, since entering a route to step
|
|
262
|
+
// straight back out of it is otherwise pure cost. runRoute steps over it.
|
|
263
|
+
if (!(
|
|
264
|
+
r.all ||
|
|
265
|
+
r.method === method ||
|
|
266
|
+
req._isOptions ||
|
|
267
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
268
|
+
)) {
|
|
269
|
+
// taken only to fail: _preprocessRequest decodes again and turns it into
|
|
270
|
+
// the error, so the handlers of a route this request cannot run never see it
|
|
271
|
+
if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
272
274
|
continue;
|
|
273
275
|
}
|
|
274
|
-
|
|
276
|
+
if (router._pathMatches(r, req)) {
|
|
277
|
+
// matched, and then stepped over: a body parser this request gets nothing
|
|
278
|
+
// out of costs a hop and answers with next() at the end of it
|
|
279
|
+
if (r.bodyParserOnly === true && stepsOver(r, req)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
286
|
}
|
|
@@ -402,7 +411,11 @@ class Walk {
|
|
|
402
411
|
useApp(req, route.mountApp);
|
|
403
412
|
}
|
|
404
413
|
const taken = mountPrefixLength(route, req);
|
|
405
|
-
|
|
414
|
+
// pushed negative when this mount consumes the whole remaining path: express invents
|
|
415
|
+
// the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
|
|
416
|
+
(req._stack ??= []).push(
|
|
417
|
+
taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
|
|
418
|
+
);
|
|
406
419
|
// a use with no path consumes nothing, so everything below would work out the values
|
|
407
420
|
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
408
421
|
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
@@ -466,6 +479,68 @@ class Walk {
|
|
|
466
479
|
return this.step(undefined);
|
|
467
480
|
}
|
|
468
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
|
|
484
|
+
* route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
|
|
485
|
+
* here without re-running step's prologue and compares.
|
|
486
|
+
*
|
|
487
|
+
* @param {boolean} isRouter next("router") rather than next("route")
|
|
488
|
+
*/
|
|
489
|
+
leaveHop(isRouter) {
|
|
490
|
+
const req = this.req;
|
|
491
|
+
const route = this.route;
|
|
492
|
+
if (route.use && !route.keepMount) {
|
|
493
|
+
const pushed = req._stack.pop();
|
|
494
|
+
const taken = pushed < 0 ? -pushed : pushed;
|
|
495
|
+
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
496
|
+
// req.url from the original path and would silently revert it. The slashAdded
|
|
497
|
+
// mangle belongs to the mount that consumed a prefix, not to a pathless use
|
|
498
|
+
if (req.url !== req._lastUrl) {
|
|
499
|
+
req._absorbUrlRewrite(taken !== 0);
|
|
500
|
+
req._consumed -= taken;
|
|
501
|
+
setMountedPath(req);
|
|
502
|
+
} else {
|
|
503
|
+
if (pushed < 0 && req._originalPath.length > req._consumed) {
|
|
504
|
+
// a rewrite below this mount left a remainder where entry had none: express
|
|
505
|
+
// strips the first character of it when it rejoins, see issue #17
|
|
506
|
+
req._originalPath =
|
|
507
|
+
req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
|
|
508
|
+
req._mayFailDecode = null;
|
|
509
|
+
}
|
|
510
|
+
if (taken !== 0) {
|
|
511
|
+
// a pathless use consumed nothing and rewrote nothing, so the recompute would
|
|
512
|
+
// write back the very values it reads
|
|
513
|
+
req._consumed -= taken;
|
|
514
|
+
setMountedPath(req);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
restoreApp(route, req);
|
|
518
|
+
}
|
|
519
|
+
if (isRouter) {
|
|
520
|
+
if (this.skipCheck) {
|
|
521
|
+
// on a compiled chain, leaving the router is what running out of chain
|
|
522
|
+
// already means: ordinary routing takes over after the mount. With no
|
|
523
|
+
// mount in the chain the router being left is the app's own, and nothing
|
|
524
|
+
// of it may run afterwards, not even a middleware registered later
|
|
525
|
+
if (this.skipUntil?.keepMount) {
|
|
526
|
+
return this.dispatch(this.routes.length);
|
|
527
|
+
}
|
|
528
|
+
return this.resolve(false);
|
|
529
|
+
}
|
|
530
|
+
// out of this router entirely, so whoever mounted it carries on after the
|
|
531
|
+
// mount. The app's own walk has nobody after it, and answers 404
|
|
532
|
+
return this.resolve(false);
|
|
533
|
+
}
|
|
534
|
+
req.routeCount++;
|
|
535
|
+
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
536
|
+
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
537
|
+
try {
|
|
538
|
+
return this.dispatch(this.routeIndex + 1);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return this.reject(err);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
469
544
|
/**
|
|
470
545
|
* One hop, which is what next() does: with nothing, run the route's next callback; with "route",
|
|
471
546
|
* leave the route; with anything else, remember it as the error and carry on.
|
|
@@ -479,39 +554,7 @@ class Walk {
|
|
|
479
554
|
const router = this.router;
|
|
480
555
|
if (thingamabob) {
|
|
481
556
|
if (thingamabob === "route" || thingamabob === "router") {
|
|
482
|
-
|
|
483
|
-
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
484
|
-
// req.url from the original path and would silently revert it
|
|
485
|
-
if (req.url !== req._lastUrl) {
|
|
486
|
-
req._absorbUrlRewrite();
|
|
487
|
-
}
|
|
488
|
-
req._consumed -= req._stack.pop();
|
|
489
|
-
setMountedPath(req);
|
|
490
|
-
restoreApp(route, req);
|
|
491
|
-
}
|
|
492
|
-
if (thingamabob === "router") {
|
|
493
|
-
if (this.skipCheck) {
|
|
494
|
-
// on a compiled chain, leaving the router is what running out of chain
|
|
495
|
-
// already means: ordinary routing takes over after the mount. With no
|
|
496
|
-
// mount in the chain the router being left is the app's own, and nothing
|
|
497
|
-
// of it may run afterwards, not even a middleware registered later
|
|
498
|
-
if (this.skipUntil?.keepMount) {
|
|
499
|
-
return this.dispatch(this.routes.length);
|
|
500
|
-
}
|
|
501
|
-
return this.resolve(false);
|
|
502
|
-
}
|
|
503
|
-
// out of this router entirely, so whoever mounted it carries on after the
|
|
504
|
-
// mount. The app's own walk has nobody after it, and answers 404
|
|
505
|
-
return this.resolve(false);
|
|
506
|
-
}
|
|
507
|
-
req.routeCount++;
|
|
508
|
-
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
509
|
-
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
510
|
-
try {
|
|
511
|
-
return this.dispatch(this.routeIndex + 1);
|
|
512
|
-
} catch (err) {
|
|
513
|
-
return this.reject(err);
|
|
514
|
-
}
|
|
557
|
+
return this.leaveHop(thingamabob === "router");
|
|
515
558
|
} else {
|
|
516
559
|
req._error = thingamabob;
|
|
517
560
|
req._errorKey = route.routeKey;
|
|
@@ -521,7 +564,7 @@ class Walk {
|
|
|
521
564
|
const kind = route.callbackKinds[this.callbackIndex];
|
|
522
565
|
const callback = route.callbacks[this.callbackIndex++];
|
|
523
566
|
if (!callback) {
|
|
524
|
-
return this.
|
|
567
|
+
return this.leaveHop(false);
|
|
525
568
|
}
|
|
526
569
|
// skipping routes we already went through via optimized path. Before the Router branch
|
|
527
570
|
// below and not after it: a mount whose chain was compiled has already run, and running it
|
|
@@ -715,6 +758,10 @@ function mountPrefixLength(route, req) {
|
|
|
715
758
|
if (route.pattern === EMPTY_REGEX) {
|
|
716
759
|
return 0;
|
|
717
760
|
}
|
|
761
|
+
// the registration-time constant of a literal mount, exec-free. See createRoute
|
|
762
|
+
if (route.mountLen !== undefined) {
|
|
763
|
+
return route.mountLen;
|
|
764
|
+
}
|
|
718
765
|
if (typeof route.pattern === "string") {
|
|
719
766
|
return route.pattern.length;
|
|
720
767
|
}
|
|
@@ -801,6 +848,61 @@ function mergesParams(route, fallback) {
|
|
|
801
848
|
return Boolean(owner?._settings?.mergeParams);
|
|
802
849
|
}
|
|
803
850
|
|
|
851
|
+
// shared empty candidate list, so _scanFrom never tests for a missing map entry twice
|
|
852
|
+
const EMPTY_INDICES = /** @type {number[]} */ ([]);
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* The generic scan's index over a router's literal routes: route positions by folded pattern, so
|
|
856
|
+
* a scan visits the routes registered for this exact path instead of comparing every one. String
|
|
857
|
+
* patterns are pure literals, everything else, "/*" included, stays in alwaysVisit and is still
|
|
858
|
+
* matched per request by _pathMatches.
|
|
859
|
+
*
|
|
860
|
+
* @param {any[]} routes the router's own table
|
|
861
|
+
* @param {boolean} caseFlag the frozen case-sensitivity flag
|
|
862
|
+
* @returns {{map: Map<string, number[]>, alwaysVisit: number[]}}
|
|
863
|
+
*/
|
|
864
|
+
function buildLiteralIndex(routes, caseFlag) {
|
|
865
|
+
const map = new Map();
|
|
866
|
+
const alwaysVisit = [];
|
|
867
|
+
for (let i = 0; i < routes.length; i++) {
|
|
868
|
+
const pattern = routes[i].pattern;
|
|
869
|
+
if (typeof pattern === "string" && pattern !== "/*") {
|
|
870
|
+
const key = caseFlag ? pattern : routes[i].patternLower;
|
|
871
|
+
const list = map.get(key);
|
|
872
|
+
if (list === undefined) {
|
|
873
|
+
map.set(key, [i]);
|
|
874
|
+
} else {
|
|
875
|
+
list.push(i);
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
alwaysVisit.push(i);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return { map, alwaysVisit };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* The position of the first value >= from in an ascending list, which is list.length when there
|
|
886
|
+
* is none: where a scan resuming at `from` enters a candidate list.
|
|
887
|
+
*
|
|
888
|
+
* @param {number[]} list
|
|
889
|
+
* @param {number} from
|
|
890
|
+
* @returns {number}
|
|
891
|
+
*/
|
|
892
|
+
function firstAtLeast(list, from) {
|
|
893
|
+
let low = 0;
|
|
894
|
+
let high = list.length;
|
|
895
|
+
while (low < high) {
|
|
896
|
+
const mid = (low + high) >> 1;
|
|
897
|
+
if (list[mid] < from) {
|
|
898
|
+
low = mid + 1;
|
|
899
|
+
} else {
|
|
900
|
+
high = mid;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return low;
|
|
904
|
+
}
|
|
905
|
+
|
|
804
906
|
/**
|
|
805
907
|
* The route's own params merged with those of the mounts it sits under, in express's order: an
|
|
806
908
|
* outer mount first, the route's own last. Numbered captures do not overwrite each other, they
|
|
@@ -1712,6 +1814,90 @@ module.exports = class Router extends EventEmitter {
|
|
|
1712
1814
|
return fullMountpath;
|
|
1713
1815
|
}
|
|
1714
1816
|
|
|
1817
|
+
/**
|
|
1818
|
+
* The generic scan over this router's own table, driven by the literal index: only the routes
|
|
1819
|
+
* registered for this exact path, plus every non-literal route, are visited, in registration
|
|
1820
|
+
* order, and each visited one still answers through the same method gate and _pathMatches the
|
|
1821
|
+
* plain loop used. The routes skipped are exactly the literals whose string compare provably
|
|
1822
|
+
* fails, so the first index this answers is the one the plain loop found.
|
|
1823
|
+
*
|
|
1824
|
+
* Runs after _freezeRoutingFlags, which is what makes _caseFlag and _strictFlag readable here
|
|
1825
|
+
* and the lazily built index stable.
|
|
1826
|
+
*
|
|
1827
|
+
* @param {any} req
|
|
1828
|
+
* @param {number} startIndex where to resume the scan
|
|
1829
|
+
* @param {boolean} mayFailDecode whether the path carries a percent escape
|
|
1830
|
+
* @returns {number} the index of the route to enter, or the table length for none
|
|
1831
|
+
*/
|
|
1832
|
+
_scanFrom(req, startIndex, mayFailDecode) {
|
|
1833
|
+
const routes = this._routes;
|
|
1834
|
+
const index = (this._literalIndex ??= buildLiteralIndex(routes, /** @type {boolean} */ (this._caseFlag)));
|
|
1835
|
+
let path = req._opPath;
|
|
1836
|
+
if (path === "") {
|
|
1837
|
+
path = "/";
|
|
1838
|
+
}
|
|
1839
|
+
if (!this._caseFlag) {
|
|
1840
|
+
path = req._opPathLower ??= path.toLowerCase();
|
|
1841
|
+
}
|
|
1842
|
+
const exact = index.map.get(path) ?? EMPTY_INDICES;
|
|
1843
|
+
// the trailing-slash twin _pathMatches allows outside strict routing, as a key: a path
|
|
1844
|
+
// "/a/" can only text-match a literal "/a", so that list joins the candidates
|
|
1845
|
+
const slashed =
|
|
1846
|
+
!this._strictFlag && path.charCodeAt(path.length - 1) === 0x2f
|
|
1847
|
+
? (index.map.get(path.slice(0, -1)) ?? EMPTY_INDICES)
|
|
1848
|
+
: EMPTY_INDICES;
|
|
1849
|
+
const always = index.alwaysVisit;
|
|
1850
|
+
let exactAt = firstAtLeast(exact, startIndex);
|
|
1851
|
+
let slashedAt = firstAtLeast(slashed, startIndex);
|
|
1852
|
+
let alwaysAt = firstAtLeast(always, startIndex);
|
|
1853
|
+
const method = req.method;
|
|
1854
|
+
const none = routes.length;
|
|
1855
|
+
for (;;) {
|
|
1856
|
+
// the next candidate in registration order, from whichever list holds it
|
|
1857
|
+
let routeIndex = none;
|
|
1858
|
+
if (exactAt < exact.length && exact[exactAt] < routeIndex) {
|
|
1859
|
+
routeIndex = exact[exactAt];
|
|
1860
|
+
}
|
|
1861
|
+
if (slashedAt < slashed.length && slashed[slashedAt] < routeIndex) {
|
|
1862
|
+
routeIndex = slashed[slashedAt];
|
|
1863
|
+
}
|
|
1864
|
+
if (alwaysAt < always.length && always[alwaysAt] < routeIndex) {
|
|
1865
|
+
routeIndex = always[alwaysAt];
|
|
1866
|
+
}
|
|
1867
|
+
if (routeIndex === none) {
|
|
1868
|
+
return none;
|
|
1869
|
+
}
|
|
1870
|
+
if (exactAt < exact.length && exact[exactAt] === routeIndex) {
|
|
1871
|
+
exactAt++;
|
|
1872
|
+
}
|
|
1873
|
+
if (slashedAt < slashed.length && slashed[slashedAt] === routeIndex) {
|
|
1874
|
+
slashedAt++;
|
|
1875
|
+
}
|
|
1876
|
+
if (alwaysAt < always.length && always[alwaysAt] === routeIndex) {
|
|
1877
|
+
alwaysAt++;
|
|
1878
|
+
}
|
|
1879
|
+
const r = routes[routeIndex];
|
|
1880
|
+
// the same gates as the plain loop, comments and all: see dispatch
|
|
1881
|
+
if (!(
|
|
1882
|
+
r.all ||
|
|
1883
|
+
r.method === method ||
|
|
1884
|
+
req._isOptions ||
|
|
1885
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
1886
|
+
)) {
|
|
1887
|
+
if (mayFailDecode && this._pathMatches(r, req) && this._paramsFailToDecode(r, req)) {
|
|
1888
|
+
return routeIndex;
|
|
1889
|
+
}
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (this._pathMatches(r, req)) {
|
|
1893
|
+
if (r.bodyParserOnly === true && stepsOver(r, req)) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
return routeIndex;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1715
1901
|
/**
|
|
1716
1902
|
* Whether a route's path matches this request. A plain string compares directly, which is what
|
|
1717
1903
|
* makes a route eligible for the native router; anything carrying a parameter or a wildcard was
|
|
@@ -1887,6 +2073,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1887
2073
|
// the "body methods" setting as it stood the first time this layer was reached,
|
|
1888
2074
|
// kept the way the parser behind it keeps it. undefined until then
|
|
1889
2075
|
bodyMethods: undefined,
|
|
2076
|
+
// a literal mount consumes exactly its registered text, so what the per-hop exec
|
|
2077
|
+
// in mountPrefixLength answers is a constant. "/" stays with the exec: its clamp
|
|
2078
|
+
// against a parent that consumed everything is not a constant
|
|
2079
|
+
mountLen:
|
|
2080
|
+
method === "USE" && typeof path === "string" && path.length > 1 && !/[:*{\\]/.test(path)
|
|
2081
|
+
? path.length
|
|
2082
|
+
: undefined,
|
|
1890
2083
|
// a mount written as a RegExp matches a piece of path that is not known until a
|
|
1891
2084
|
// request comes in, so its stack entry cannot be the path itself
|
|
1892
2085
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
@@ -1937,6 +2130,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1937
2130
|
routes.push(route);
|
|
1938
2131
|
}
|
|
1939
2132
|
this._routes.push(...routes);
|
|
2133
|
+
// the literal index positions are stale the moment the table grows
|
|
2134
|
+
this._literalIndex = undefined;
|
|
1940
2135
|
|
|
1941
2136
|
// anything registered after listen invalidates what the header-skip analysis proved:
|
|
1942
2137
|
// it could catch a throw or read what a chain never did, so every skip is taken back
|
|
@@ -2396,14 +2591,23 @@ module.exports = class Router extends EventEmitter {
|
|
|
2396
2591
|
needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
|
|
2397
2592
|
)
|
|
2398
2593
|
: null;
|
|
2399
|
-
const makeHandler = (chain, preset, skips) => {
|
|
2594
|
+
const makeHandler = (chain, preset, skips, wireMethod) => {
|
|
2400
2595
|
// the mutable object a granted skip lives on, so a middleware arriving after
|
|
2401
2596
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
2402
|
-
// parameterised one, which has no preset, gets a holder of its own
|
|
2597
|
+
// parameterised one, which has no preset, gets a holder of its own. It also carries
|
|
2598
|
+
// the registration's method, so the constructor settles it with one compare
|
|
2403
2599
|
let skipHolder = preset;
|
|
2404
|
-
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
|
|
2405
|
-
skipHolder = {
|
|
2406
|
-
|
|
2600
|
+
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
|
|
2601
|
+
skipHolder = {
|
|
2602
|
+
skipHeaders: skips.skipHeaders,
|
|
2603
|
+
skipQuery: skips.skipQuery,
|
|
2604
|
+
method: wireMethod,
|
|
2605
|
+
isOptions: wireMethod === "OPTIONS",
|
|
2606
|
+
isHead: wireMethod === "HEAD"
|
|
2607
|
+
};
|
|
2608
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
2609
|
+
(this._skipPresets ??= new Set()).add(skipHolder);
|
|
2610
|
+
}
|
|
2407
2611
|
}
|
|
2408
2612
|
// all three are registration-time constants: computing them in the handler was a
|
|
2409
2613
|
// closure and a scan of the chain on every native request.
|
|
@@ -2430,6 +2634,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2430
2634
|
return this._refuseRequest(response);
|
|
2431
2635
|
}
|
|
2432
2636
|
if (optimizedParams) {
|
|
2637
|
+
// slicing these out of the already-fetched path instead measured a wash:
|
|
2638
|
+
// the segment scan costs what the crossing costs
|
|
2433
2639
|
request.optimizedParams = new NullObject();
|
|
2434
2640
|
for (let i = 0; i < optimizedParams.length; i++) {
|
|
2435
2641
|
request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
|
|
@@ -2475,8 +2681,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2475
2681
|
// analysis in usage.js, whose default answer is no.
|
|
2476
2682
|
//
|
|
2477
2683
|
// The etag setting is not one of the conditions. It used to be, on the grounds that send
|
|
2478
|
-
// consults freshness, but the skip branch reads if-none-match
|
|
2479
|
-
//
|
|
2684
|
+
// consults freshness, but the skip branch reads if-none-match and if-modified-since by
|
|
2685
|
+
// name whatever the setting, see the comment at request.js:527, and
|
|
2480
2686
|
// req.fresh reads nothing else off the request. Requiring etag off as well cost the copy
|
|
2481
2687
|
// to every application that left it on, which is every application that did not go
|
|
2482
2688
|
// looking for the setting.
|
|
@@ -2508,10 +2714,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
2508
2714
|
return preset;
|
|
2509
2715
|
};
|
|
2510
2716
|
|
|
2717
|
+
// the wire token this registration answers; "any" serves every verb and stays dynamic
|
|
2718
|
+
const wireMethod = method === "any" ? null : route.method;
|
|
2511
2719
|
let fn = makeHandler(
|
|
2512
2720
|
getChain,
|
|
2513
2721
|
canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
|
|
2514
|
-
getSkips
|
|
2722
|
+
getSkips,
|
|
2723
|
+
wireMethod
|
|
2515
2724
|
);
|
|
2516
2725
|
const jsFn = fn;
|
|
2517
2726
|
|
|
@@ -2564,7 +2773,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
2564
2773
|
fn !== jsFn
|
|
2565
2774
|
? fn
|
|
2566
2775
|
: canPreset
|
|
2567
|
-
? makeHandler(
|
|
2776
|
+
? makeHandler(
|
|
2777
|
+
getChain,
|
|
2778
|
+
makePreset(route.path + "/", route.method, getSkips),
|
|
2779
|
+
getSkips,
|
|
2780
|
+
wireMethod
|
|
2781
|
+
)
|
|
2568
2782
|
: fn;
|
|
2569
2783
|
this.uwsApp[method](replacedPath + "/", slashFn);
|
|
2570
2784
|
if (method === "get") {
|
|
@@ -2573,7 +2787,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2573
2787
|
makeHandler(
|
|
2574
2788
|
headChain,
|
|
2575
2789
|
canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
|
|
2576
|
-
headSkips
|
|
2790
|
+
headSkips,
|
|
2791
|
+
"HEAD"
|
|
2577
2792
|
)
|
|
2578
2793
|
);
|
|
2579
2794
|
}
|
|
@@ -2582,7 +2797,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
2582
2797
|
// its own handler always: the shared one would carry the GET registration's method
|
|
2583
2798
|
this.uwsApp.head(
|
|
2584
2799
|
replacedPath,
|
|
2585
|
-
makeHandler(
|
|
2800
|
+
makeHandler(
|
|
2801
|
+
headChain,
|
|
2802
|
+
canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined,
|
|
2803
|
+
headSkips,
|
|
2804
|
+
"HEAD"
|
|
2805
|
+
)
|
|
2586
2806
|
);
|
|
2587
2807
|
}
|
|
2588
2808
|
}
|
|
@@ -2741,14 +2961,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
2741
2961
|
raiseDecodeFailure(req, route, err);
|
|
2742
2962
|
return "route";
|
|
2743
2963
|
}
|
|
2744
|
-
|
|
2964
|
+
// the stack check first: it is two field loads, mergesParams is a call, and almost
|
|
2965
|
+
// no request carries a param stack at all
|
|
2966
|
+
if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
|
|
2745
2967
|
req.params = mergeParams(req.params, req._paramStack);
|
|
2746
2968
|
}
|
|
2747
2969
|
} else {
|
|
2748
2970
|
// express 5 gives every matched route null-prototype params; only a pathless
|
|
2749
2971
|
// middleware layer keeps the plain object, as its router hands one to fast_slash
|
|
2750
2972
|
req.params = route.use && route.path === "" ? {} : Object.create(null);
|
|
2751
|
-
if (
|
|
2973
|
+
if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
|
|
2752
2974
|
req.params = mergeParams(req.params, req._paramStack);
|
|
2753
2975
|
}
|
|
2754
2976
|
}
|
|
@@ -3132,8 +3354,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
3132
3354
|
return;
|
|
3133
3355
|
}
|
|
3134
3356
|
response.status(404);
|
|
3135
|
-
// the
|
|
3136
|
-
|
|
3357
|
+
// the pathname of originalUrl, as express's finalhandler prints it: _originalPath absorbs
|
|
3358
|
+
// a req.url rewrite, originalUrl never changes
|
|
3359
|
+
const originalUrl = String(request.originalUrl);
|
|
3360
|
+
const queryIndex = originalUrl.indexOf("?");
|
|
3361
|
+
const pathname = queryIndex === -1 ? originalUrl : originalUrl.slice(0, queryIndex);
|
|
3362
|
+
this._sendErrorPage(request, response, `Cannot ${request.method} ${pathname}`, false);
|
|
3137
3363
|
}
|
|
3138
3364
|
};
|
|
3139
3365
|
|
package/src/types.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ declare module "fulmine.js" {
|
|
|
50
50
|
enforceEncoding?: string;
|
|
51
51
|
/** Brotli options. The default quality is 4. */
|
|
52
52
|
brotli?: BrotliOptions;
|
|
53
|
+
/**
|
|
54
|
+
* The encodings this middleware may answer with; what is not named is never used,
|
|
55
|
+
* however the client ranks it. Fulmine's own option, the compression module has no
|
|
56
|
+
* equivalent. An uncompressed answer is always on offer.
|
|
57
|
+
*/
|
|
58
|
+
encodings?: ("br" | "gzip" | "deflate" | "identity")[];
|
|
53
59
|
}
|
|
54
60
|
// what listen() decided about each route, for a test to hold on to
|
|
55
61
|
interface RouteVerdict {
|
package/src/utils.js
CHANGED
|
@@ -1510,6 +1510,11 @@ function validateHeaderName(name) {
|
|
|
1510
1510
|
}
|
|
1511
1511
|
}
|
|
1512
1512
|
|
|
1513
|
+
// values already accepted, so the constant strings middleware writes per request, helmet's CSP
|
|
1514
|
+
// among them, skip the scan. Insert-only after the regex accepts, so a hit cannot change a
|
|
1515
|
+
// verdict; bounded, and set-cookie values are per-user so they stay out
|
|
1516
|
+
const KNOWN_HEADER_VALUES = new Set();
|
|
1517
|
+
|
|
1513
1518
|
/**
|
|
1514
1519
|
* Refuses a header value holding a character that cannot go on the wire, with node's error. An
|
|
1515
1520
|
* array is sent as one header per entry, so each entry is checked on its own rather than as the
|
|
@@ -1527,9 +1532,19 @@ function validateHeaderValue(name, value) {
|
|
|
1527
1532
|
}
|
|
1528
1533
|
return;
|
|
1529
1534
|
}
|
|
1535
|
+
if (KNOWN_HEADER_VALUES.has(value)) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1530
1538
|
if (HEADER_VALUE.test(value)) {
|
|
1531
1539
|
throw headerError(`Invalid character in header content ["${name}"]`, "ERR_INVALID_CHAR");
|
|
1532
1540
|
}
|
|
1541
|
+
if (
|
|
1542
|
+
KNOWN_HEADER_VALUES.size < 512 &&
|
|
1543
|
+
value.length <= 1024 &&
|
|
1544
|
+
!(name.length === 10 && name.toLowerCase() === "set-cookie")
|
|
1545
|
+
) {
|
|
1546
|
+
KNOWN_HEADER_VALUES.add(value);
|
|
1547
|
+
}
|
|
1533
1548
|
}
|
|
1534
1549
|
|
|
1535
1550
|
/**
|
|
@@ -1629,6 +1644,7 @@ module.exports = {
|
|
|
1629
1644
|
negotiateEncoding,
|
|
1630
1645
|
ENCODING_BR,
|
|
1631
1646
|
ENCODING_GZIP,
|
|
1647
|
+
ENCODING_DEFLATE,
|
|
1632
1648
|
ENCODING_ANY,
|
|
1633
1649
|
memoizeByString,
|
|
1634
1650
|
isRangeFresh,
|