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/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
- // written out rather than through a predicate handed to findIndexStartingFrom, which
246
- // was one closure per hop of every request not on a compiled chain
247
- for (; routeIndex < routes.length; routeIndex++) {
248
- const r = routes[routeIndex];
249
- // A HEAD request enters a route whose path matched even when its verb cannot serve
250
- // one: express exempts HEAD from the method check ("if (!hasMethod && method !==
251
- // 'HEAD')" in router/index.js), so the layer's parameters are captured and its
252
- // param() callbacks run before the route is dropped. Only asked when the router has
253
- // callbacks to run, since entering a route to step straight back out of it is
254
- // otherwise pure cost with nothing to show for it. runRoute steps over it.
255
- if (!(
256
- r.all ||
257
- r.method === req.method ||
258
- req._isOptions ||
259
- (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
260
- )) {
261
- // taken only to fail: _preprocessRequest decodes again and turns it into the
262
- // error, so the handlers of a route this request cannot run never see it
263
- if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
264
- break;
265
- }
266
- continue;
267
- }
268
- if (router._pathMatches(r, req)) {
269
- // matched, and then stepped over: a body parser this request gets nothing out
270
- // of costs a hop and answers with next() at the end of it
271
- if (r.bodyParserOnly === true && stepsOver(r, req)) {
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
- break;
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
- (req._stack ??= []).push(taken);
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
- if (route.use && !route.keepMount) {
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.step("route");
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 = { skipHeaders: skips.skipHeaders, skipQuery: skips.skipQuery };
2406
- (this._skipPresets ??= new Set()).add(skipHolder);
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, if-modified-since and
2479
- // cache-control by name whatever the setting, see the comment at request.js:527, and
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(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips)
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(headChain, canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined, headSkips)
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
- if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
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 (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
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 whole path, not what a mount left behind in req.path
3136
- this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
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,