fulmine.js 5.2.0 → 5.3.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
@@ -2,6 +2,8 @@
2
2
  Copyright 2024 dimden.dev
3
3
  Copyright 2026 Nigro Simone
4
4
 
5
+ This file is derived from Ultimate Express and has been modified.
6
+
5
7
  Licensed under the Apache License, Version 2.0 (the "License");
6
8
  you may not use this file except in compliance with the License.
7
9
  You may obtain a copy of the License at
@@ -17,7 +19,6 @@ limitations under the License.
17
19
 
18
20
  const {
19
21
  patternToRegex,
20
- escapePathLiteral,
21
22
  getPatternMeta,
22
23
  decodeParam,
23
24
  needsConversionToRegex,
@@ -36,9 +37,70 @@ const compileDeclarative = require("./declarative.js");
36
37
  const statuses = require("statuses");
37
38
  const { METHODS } = require("http");
38
39
  const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
39
- const { chainUsage } = require("./usage.js");
40
+ const { chainUsage, kGetSafe } = require("./usage.js");
40
41
  const { checkBehavior } = require("./websocket.js");
41
42
 
43
+ // whether a registered path could be asked for in another case, which is what decides whether the
44
+ // native router can be trusted to prefer it, see _optimizeRoute
45
+ const HAS_LETTER = /[a-zA-Z]/;
46
+
47
+ // hands out one number per app.route(), so the routes it creates know they belong together
48
+ let routeGroups = 0;
49
+
50
+ /**
51
+ * Whether an earlier route would have answered this path had case not mattered. A guard is a
52
+ * folded string when the earlier path is a literal, and an insensitive pattern when it has
53
+ * parameters of its own.
54
+ *
55
+ * The string side is compared character by character rather than through toLowerCase, because it
56
+ * sits on the hot path of every parameter route that has an earlier literal, and saying no must
57
+ * allocate nothing.
58
+ *
59
+ * @param {(string|RegExp)[]} guards
60
+ * @param {string} path the path as it arrived
61
+ * @returns {boolean}
62
+ */
63
+ function anyGuardHits(guards, path) {
64
+ for (let i = 0; i < guards.length; i++) {
65
+ const guard = guards[i];
66
+ if (typeof guard !== "string") {
67
+ if (guard.test(path)) {
68
+ return true;
69
+ }
70
+ continue;
71
+ }
72
+ // the guard is a registered path, which under the default routing answers the same path
73
+ // with one trailing slash as well: "/x1" registered is what serves "/x1/", so "/X1/" is
74
+ // just as much a case variant of it as "/X1" is. Missing that answered "/X1/" from the
75
+ // parameter route behind it while express answered from the literal.
76
+ //
77
+ // The regex guards, for earlier paths that carry parameters of their own, are built
78
+ // non-strict and already accept it. Erring wide costs nothing here either: a guard that
79
+ // hits only hands the request to the generic router, which is where express's own order
80
+ // decides anyway
81
+ const slashed = path.length === guard.length + 1 && path.charCodeAt(guard.length) === 0x2f;
82
+ if (guard.length !== path.length && !slashed) {
83
+ continue;
84
+ }
85
+ let same = true;
86
+ for (let j = 0; j < guard.length; j++) {
87
+ let code = path.charCodeAt(j);
88
+ // A to Z only, which is the fold express's insensitive routing does
89
+ if (code >= 65 && code <= 90) {
90
+ code += 32;
91
+ }
92
+ if (code !== guard.charCodeAt(j)) {
93
+ same = false;
94
+ break;
95
+ }
96
+ }
97
+ if (same) {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+
42
104
  // every method the declarative compiler can emit: a patched one must disable compilation, or the
43
105
  // patch would be honoured everywhere but on compiled routes
44
106
  const resCodes = {},
@@ -89,6 +151,29 @@ class Walk {
89
151
  // bound, not wrapped in an arrow: an arrow forwarding into step() is one more call on every
90
152
  // hop, and it measured 495 microseconds per thousand requests of nothing else
91
153
  this.next = this.step.bind(this);
154
+ // What res.sendFile reports a failure to. Express hands it req.next, which is the router
155
+ // next and not the route one, so a file that cannot be served leaves the route and its
156
+ // error reaches the router error handlers rather than a four argument handler written
157
+ // inside the route. req.next itself is left alone: making it mean this everywhere is what
158
+ // express does, and it breaks express own res.format and app.routes.error tests here, so
159
+ // that stays open rather than half done.
160
+ this.leaveRoute = this.stepOutOfRoute.bind(this);
161
+ }
162
+
163
+ /**
164
+ * Leaves the rest of this route, with the error if there is one, and carries on with the route
165
+ * after it.
166
+ *
167
+ * @param {any} [err]
168
+ */
169
+ stepOutOfRoute(err) {
170
+ if (err) {
171
+ const req = this.req;
172
+ req._error = err;
173
+ req._errorKey = this.route.routeKey;
174
+ req._errorGroup = this.route.group;
175
+ }
176
+ this.step("route");
92
177
  }
93
178
 
94
179
  /**
@@ -108,15 +193,40 @@ class Walk {
108
193
  return;
109
194
  }
110
195
  let routeIndex = startIndex;
196
+ // a compiled chain runs what is in it without matching again, so this is where a layer that
197
+ // provably has nothing to do for this request is stepped over rather than entered
198
+ if (this.skipCheck) {
199
+ while (routeIndex < routes.length && routes[routeIndex].bodyParserOnly === true) {
200
+ if (!stepsOver(routes[routeIndex], req)) {
201
+ break;
202
+ }
203
+ routeIndex++;
204
+ }
205
+ }
111
206
  if (!this.skipCheck) {
207
+ // express matches a layer's path before it looks at the method, and decodes the
208
+ // parameters there, so a malformed escape answers 400 even when no route of this
209
+ // method exists. Only a path carrying a percent can produce one, and that check keeps
210
+ // every other request from matching routes it could never run
211
+ const mayFailDecode = req._originalPath.indexOf("%") !== -1;
112
212
  // written out rather than through a predicate handed to findIndexStartingFrom, which
113
213
  // was one closure per hop of every request not on a compiled chain
114
214
  for (; routeIndex < routes.length; routeIndex++) {
115
215
  const r = routes[routeIndex];
116
- if (
117
- (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) &&
118
- router._pathMatches(r, req)
119
- ) {
216
+ if (!(r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead))) {
217
+ // taken only to fail: _preprocessRequest decodes again and turns it into the
218
+ // error, so the handlers of a route this request cannot run never see it
219
+ if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
220
+ break;
221
+ }
222
+ continue;
223
+ }
224
+ if (router._pathMatches(r, req)) {
225
+ // matched, and then stepped over: a body parser this request gets nothing out
226
+ // of costs a hop and answers with next() at the end of it
227
+ if (r.bodyParserOnly === true && stepsOver(r, req)) {
228
+ continue;
229
+ }
120
230
  break;
121
231
  }
122
232
  }
@@ -134,19 +244,14 @@ class Walk {
134
244
  // path is still relative to it. /alone/skip must not be offered to the app as /skip
135
245
  if (req._stack !== null && req._stack.length > 0) {
136
246
  req._stack.length = 0;
137
- req._stackMounted = 0;
138
- req.path = req._originalPath;
139
- req.url = req._originalPath + req.urlQuery;
140
- req._opPath =
141
- req.endsWithSlash && req._originalPath !== "/" && !router.get("strict routing")
142
- ? req._originalPath.slice(0, -1)
143
- : req._originalPath;
144
- req._lastUrl = req.url;
247
+ req._consumed = 0;
248
+ setMountedPath(req);
145
249
  }
146
250
  // an error out of a mount is attributed to the mount, so error handlers declared before
147
251
  // it do not catch it, as in ordinary dispatch
148
252
  if (req._error && this.skipUntil && this.skipUntil.keepMount && this.skipUntil.routeKey > req._errorKey) {
149
253
  req._errorKey = this.skipUntil.routeKey;
254
+ req._errorGroup = this.skipUntil.group;
150
255
  }
151
256
  this.routes = router._routes;
152
257
  this.skipCheck = false;
@@ -192,14 +297,8 @@ class Walk {
192
297
  this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
193
298
  if (req._stack !== null && req._stack.length > 0) {
194
299
  req._stack.length = 0;
195
- req._stackMounted = 0;
196
- req.path = req._originalPath;
197
- req.url = req._originalPath + req.urlQuery;
198
- req._opPath =
199
- req.endsWithSlash && req._originalPath !== "/" && !router.get("strict routing")
200
- ? req._originalPath.slice(0, -1)
201
- : req._originalPath;
202
- req._lastUrl = req.url;
300
+ req._consumed = 0;
301
+ setMountedPath(req);
203
302
  }
204
303
  this.routes = router._routes;
205
304
  this.skipCheck = false;
@@ -216,37 +315,25 @@ class Walk {
216
315
  runRoute(continueRoute) {
217
316
  const req = this.req;
218
317
  const route = this.route;
219
- const router = this.router;
220
318
  if (route.use) {
221
319
  if (route.mountApp) {
222
320
  // optimized chain: normal dispatch swaps req.app when it enters a mounted
223
321
  // Application, but the compiled mount route has no callback to do it
322
+ rememberApp(this, route, req);
224
323
  useApp(req, route.mountApp);
225
324
  }
226
- (req._stack ??= []).push(route.regexMount ? regexMountEntry(router, route, req) : route.path);
325
+ const taken = mountPrefixLength(route, req);
326
+ (req._stack ??= []).push(taken);
227
327
  // a use with no path consumes nothing, so everything below would work out the values
228
328
  // that are already there. Only skipped without a trailing slash, where the rules about
229
329
  // one cannot bite. An application is mostly pathless middleware, and this is per hop
230
- if (route.path !== "" || req.endsWithSlash) {
231
- if (route.path !== "") {
232
- req._stackMounted++;
233
- }
234
- const fullMountpath = router.getFullMountpath(req);
235
- req._opPath =
236
- fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, "") : req._originalPath;
237
- if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
238
- req._opPath = router.get("strict routing") ? req._opPath + "/" : req._opPath.slice(0, -1);
239
- }
240
- req.url = req._opPath + req.urlQuery;
241
- req.path = req._opPath;
242
- if (req._opPath === "") {
243
- req.url = "/" + req.urlQuery;
244
- req.path = "/";
245
- }
246
- req._lastUrl = req.url;
330
+ if (taken !== 0 || req.endsWithSlash) {
331
+ req._consumed += taken;
332
+ setMountedPath(req);
247
333
  }
248
334
  }
249
335
  req.next = this.next;
336
+ req._leaveRoute = this.leaveRoute;
250
337
  if (continueRoute === "route") {
251
338
  this.step("route");
252
339
  } else if (continueRoute) {
@@ -266,7 +353,15 @@ class Walk {
266
353
  errorHop(kind, callback) {
267
354
  const req = this.req;
268
355
  const route = this.route;
269
- if (req._error && kind === CALLBACK_ERROR && route.routeKey >= req._errorKey) {
356
+ // A four argument handler written inside a route only ever sees what that route raised:
357
+ // express skips a route layer entirely while an error is in flight, so an error from a
358
+ // middleware before it, or out of a mount, walks past to the router's own error handlers.
359
+ // Middleware error handlers keep the ordinary rule, which is that they catch what was
360
+ // raised before them.
361
+ const reachable = route.use
362
+ ? route.routeKey >= req._errorKey
363
+ : route.routeKey === req._errorKey || (route.group !== undefined && route.group === req._errorGroup);
364
+ if (req._error && kind === CALLBACK_ERROR && reachable) {
270
365
  const out = this.router._handleError(req._error, callback, req, this.res);
271
366
  if (out instanceof Promise) {
272
367
  // an error handler's rejected promise moves on to the next error handler, and
@@ -274,6 +369,7 @@ class Walk {
274
369
  out.catch((err) => {
275
370
  req._error = err || new Error("Rejected promise");
276
371
  req._errorKey = route.routeKey;
372
+ req._errorGroup = route.group;
277
373
  return this.step(undefined);
278
374
  });
279
375
  }
@@ -301,45 +397,20 @@ class Walk {
301
397
  if (req.url !== req._lastUrl) {
302
398
  req._absorbUrlRewrite();
303
399
  }
304
- if (req._stack.pop() !== "") {
305
- req._stackMounted--;
306
- }
307
-
308
- const strictRouting = router.get("strict routing");
309
- const poppedMountpath = req._stack.length > 0 ? router.getFullMountpath(req) : EMPTY_REGEX;
310
- req._opPath =
311
- poppedMountpath !== EMPTY_REGEX
312
- ? req._originalPath.replace(poppedMountpath, "")
313
- : req._originalPath;
314
- if (strictRouting) {
315
- if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
316
- req._opPath += "/";
317
- }
318
- }
319
- req.url = req._opPath + req.urlQuery;
320
- req.path = req._opPath;
321
- if (req._opPath === "") {
322
- req.url = "/" + req.urlQuery;
323
- req.path = "/";
324
- }
325
- req._lastUrl = req.url;
326
- if (
327
- !strictRouting &&
328
- req.endsWithSlash &&
329
- req._originalPath !== "/" &&
330
- req._opPath[req._opPath.length - 1] === "/"
331
- ) {
332
- req._opPath = req._opPath.slice(0, -1);
333
- }
334
- if (req.app.parent && route.callbacks[0]?.constructor.name === "Application") {
335
- useApp(req, req.app.parent);
336
- }
400
+ req._consumed -= req._stack.pop();
401
+ setMountedPath(req);
402
+ restoreApp(route, req);
337
403
  }
338
404
  if (thingamabob === "router") {
339
405
  if (this.skipCheck) {
340
406
  // on a compiled chain, leaving the router is what running out of chain
341
- // already means: ordinary routing takes over after the mount
342
- return this.dispatch(this.routes.length);
407
+ // already means: ordinary routing takes over after the mount. With no
408
+ // mount in the chain the router being left is the app's own, and nothing
409
+ // of it may run afterwards, not even a middleware registered later
410
+ if (this.skipUntil?.keepMount) {
411
+ return this.dispatch(this.routes.length);
412
+ }
413
+ return this.resolve(false);
343
414
  }
344
415
  // out of this router entirely, so whoever mounted it carries on after the
345
416
  // mount. The app's own walk has nobody after it, and answers 404
@@ -356,6 +427,7 @@ class Walk {
356
427
  } else {
357
428
  req._error = thingamabob;
358
429
  req._errorKey = route.routeKey;
430
+ req._errorGroup = route.group;
359
431
  }
360
432
  }
361
433
  const kind = route.callbackKinds[this.callbackIndex];
@@ -371,6 +443,7 @@ class Walk {
371
443
  }
372
444
  if (kind === CALLBACK_ROUTER) {
373
445
  if (callback.constructor.name === "Application") {
446
+ rememberApp(this, route, req);
374
447
  useApp(req, callback);
375
448
  }
376
449
  const pushedParams = callback.settings.mergeParams;
@@ -380,12 +453,12 @@ class Walk {
380
453
  // express restores req.params when a router hands back, so what runs after the mount
381
454
  // sees the params it had before it
382
455
  const parentParams = req.params;
383
- if (
384
- callback.settings["strict routing"] &&
385
- req.endsWithSlash &&
386
- req._opPath[req._opPath.length - 1] !== "/"
387
- ) {
388
- req._opPath += "/";
456
+ // each router answers OPTIONS with the verbs it knows itself, so the one being entered
457
+ // starts its own list: express keeps that list per router, and a router that hands back
458
+ // without answering leaves the outer one's untouched
459
+ const parentMethods = req._matchedMethods;
460
+ if (parentMethods !== null) {
461
+ req._matchedMethods = new Set();
389
462
  }
390
463
  callback
391
464
  ._routeRequest(req, res, 0)
@@ -397,15 +470,25 @@ class Walk {
397
470
  req.params = parentParams;
398
471
  if (req._error) {
399
472
  req._errorKey = route.routeKey;
473
+ req._errorGroup = route.group;
474
+ }
475
+ if (routed) {
476
+ if (parentMethods !== null) {
477
+ req._matchedMethods = parentMethods;
478
+ }
479
+ return this.resolve(true);
480
+ }
481
+ const childMethods = req._matchedMethods;
482
+ if (parentMethods !== null) {
483
+ req._matchedMethods = parentMethods;
400
484
  }
401
- if (routed) return this.resolve(true);
402
- if (req._isOptions && req._matchedMethods.size) {
485
+ if (req._isOptions && childMethods.size) {
403
486
  // OPTIONS routing is different, it stops in the router if matched.
404
487
  // Express answers as the router hands back, so a throw while answering,
405
488
  // a head already written being the way, walks on to later error handlers
406
489
  if (!req._error) {
407
490
  try {
408
- router._sendOptionsReply(req, res);
491
+ router._sendOptionsReply(req, res, childMethods);
409
492
  return this.resolve(true);
410
493
  } catch (err) {
411
494
  return this.step(err);
@@ -445,12 +528,14 @@ class Walk {
445
528
  out.catch((err) => {
446
529
  req._error = err || new Error("Rejected promise");
447
530
  req._errorKey = route.routeKey;
531
+ req._errorGroup = route.group;
448
532
  return this.step(undefined);
449
533
  });
450
534
  }
451
535
  } catch (err) {
452
536
  req._error = err;
453
537
  req._errorKey = route.routeKey;
538
+ req._errorGroup = route.group;
454
539
  return this.step(undefined);
455
540
  }
456
541
  }
@@ -513,22 +598,62 @@ function nativeFail(err) {
513
598
  }
514
599
 
515
600
  /**
516
- * What a RegExp mount consumed of the path, escaped so the mount-stack join can compile it as a
517
- * literal. Exec runs on the same fixed-up path _pathMatches tested: a parent mount that consumed
601
+ * How much of the path a mount takes, which is what its own pattern matched and never more than
602
+ * there is. Exec runs on the same fixed-up path _pathMatches tested: a parent mount that consumed
518
603
  * everything leaves "", where the pattern was matched against "/".
519
604
  *
520
- * @param {any} router
605
+ * Counting what each mount took, rather than rebuilding one pattern out of the whole stack and
606
+ * matching that against the original path, is the difference between a sum and a guess: a mount
607
+ * written as an optional group composes into a pattern the path no longer satisfies, and the
608
+ * prefix stayed on.
609
+ *
521
610
  * @param {any} route
522
611
  * @param {any} req
523
- * @returns {string}
612
+ * @returns {number}
524
613
  */
525
- function regexMountEntry(router, route, req) {
526
- let mountPath = req._opPath;
527
- if (req.endsWithSlash && mountPath.endsWith("/") && !router.get("strict routing")) {
528
- mountPath = mountPath.slice(0, -1);
614
+ function mountPrefixLength(route, req) {
615
+ const path = req._opPath;
616
+ if (typeof route.pattern === "string") {
617
+ return route.pattern.length;
529
618
  }
530
- const matched = route.pattern.exec(mountPath === "" ? "/" : mountPath);
531
- return matched ? escapePathLiteral(matched[0]) : "";
619
+ const matched = route.pattern.exec(path === "" ? "/" : path);
620
+ return matched ? Math.min(matched[0].length, path.length) : 0;
621
+ }
622
+
623
+ /**
624
+ * Writes the path the routes below a mount see: the original with what the mounts took off the
625
+ * front. The root reads as "/" rather than as nothing, which is how express hands it over.
626
+ *
627
+ * @param {any} req
628
+ */
629
+ function setMountedPath(req) {
630
+ req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
631
+ req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
632
+ req.path = req._opPath === "" ? "/" : req._opPath;
633
+ req._lastUrl = req.url;
634
+ }
635
+
636
+ /**
637
+ * The route's own params merged with those of the mounts it sits under, in express's order: an
638
+ * outer mount first, the route's own last. Numbered captures do not overwrite each other, they
639
+ * shift, so a RegExp mount capturing one group leaves the route's own group numbered from one.
640
+ *
641
+ * @param {Record<string, any>} own what this route's own pattern captured
642
+ * @param {Record<string, any>[]} stack the mounts, outermost first
643
+ * @returns {Record<string, any>}
644
+ */
645
+ /**
646
+ * Whether this route reads the parameters of the mounts above it, which is its own router asking
647
+ * for them. The stack holds what a mergeParams router captured on the way in, and a plain router
648
+ * mounted inside one must not read it: express asks each router in turn, not the outermost.
649
+ *
650
+ * @param {any} route
651
+ * @param {any} fallback the router dispatching, when the route names no owner
652
+ * @returns {boolean}
653
+ */
654
+ function mergesParams(route, fallback) {
655
+ const owner = route.owner ?? fallback;
656
+ return Boolean(owner?.settings?.mergeParams);
532
657
  }
533
658
 
534
659
  /**
@@ -628,7 +753,7 @@ function adoptPlainRequest(req, router) {
628
753
  req.originalUrl = req.originalUrl ?? arrived;
629
754
  req._originalPath = path;
630
755
  req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
631
- req._opPath = req.endsWithSlash && path !== "/" && !router.get("strict routing") ? path.slice(0, -1) : path;
756
+ req._opPath = path;
632
757
  req._lastUrl = req.url;
633
758
  req._isOptions = req.method === "OPTIONS";
634
759
  req._isHead = req.method === "HEAD";
@@ -636,7 +761,7 @@ function adoptPlainRequest(req, router) {
636
761
  // null, not fresh arrays: the push sites materialize them on the first mount, and most
637
762
  // requests never see one, same as the Request constructor
638
763
  req._stack = null;
639
- req._stackMounted = 0;
764
+ req._consumed = 0;
640
765
  req._paramStack = null;
641
766
  req._matchedMethods = req._isOptions ? new Set() : null;
642
767
  req.routeCount = 1;
@@ -673,9 +798,6 @@ function onNativeAborted() {
673
798
  response.socket?.emit("error", err);
674
799
  }
675
800
 
676
- /**
677
- *
678
- */
679
801
  /**
680
802
  * The per-request constants of a fully literal native registration. µWS matched the URL byte for
681
803
  * byte against this exact pattern and dispatches by method, so the request constructor can take
@@ -683,15 +805,14 @@ function onNativeAborted() {
683
805
  *
684
806
  * @param {string} path the registered pattern, which is what getUrl() would have answered
685
807
  * @param {string} method uppercase, fixed by which uWS verb the registration used
686
- * @param {boolean} strict the owner's strict routing, frozen here like the twin registration is
687
808
  */
688
- function nativePreset(path, method, strict) {
809
+ function nativePreset(path, method) {
689
810
  const endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
690
811
  return {
691
812
  path,
692
813
  method,
693
814
  endsWithSlash,
694
- opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
815
+ opPath: path,
695
816
  isOptions: method === "OPTIONS",
696
817
  isHead: method === "HEAD",
697
818
  // set at registration when the whole chain provably never reads a header, or never
@@ -752,6 +873,138 @@ const CALLBACK_ROUTER = 2;
752
873
  * @param {any} req
753
874
  * @param {any} app
754
875
  */
876
+ /**
877
+ * Reports a parameter that will not decode, unless something is already being reported.
878
+ *
879
+ * Matching a route decodes its parameters, and that happens while the walk is still looking for
880
+ * whoever should answer, including when it is looking for an error handler. Express does the same
881
+ * and keeps the first error it has: `layerError = layerError || match` in its router. Overwriting
882
+ * meant a middleware that had already refused the path, express.static answering Bad Request on an
883
+ * escape it could not decode, had its answer replaced by the decode failure of a route further down
884
+ * that was never going to run. Same status, different message, and only when a later route happens
885
+ * to match the same path. Found by fuzzing route tables against express.
886
+ *
887
+ * @param {any} req
888
+ * @param {any} route
889
+ * @param {any} err
890
+ */
891
+ function raiseDecodeFailure(req, route, err) {
892
+ if (req._error) {
893
+ return;
894
+ }
895
+ req._error = err;
896
+ req._errorKey = route.routeKey;
897
+ req._errorGroup = route.group;
898
+ }
899
+
900
+ // the verbs a body is read for unless the application says otherwise, which is the parsers' own
901
+ // list. A request with any other verb reaches a parser's method check and leaves through it
902
+ const BODY_METHODS = new Set(["POST", "PUT", "PATCH", "QUERY"]);
903
+
904
+ /**
905
+ * Whether this layer can be stepped over for this request without changing a thing.
906
+ *
907
+ * Only the body parsers are ever asked. Their prologue leaves a request that said nothing about a
908
+ * body alone, whatever content type it carries, which is what `kGetSafe` already records for the
909
+ * header-skip analysis. Two conditions on top of that mark, and both are needed:
910
+ *
911
+ * The request must have said nothing about framing at all, a `content-length: 0` included. A parser
912
+ * that can see a length answers about the body it describes even when that body is empty: a zero
913
+ * length with a charset nobody can decode is a 415, in express and here.
914
+ *
915
+ * And the verb must be one no parser reads a body for. With no length and no transfer-encoding a
916
+ * POST still walks into the read, comes back with nothing, and leaves `req.body` as the empty value
917
+ * its parser produces, which is a thing a handler can see.
918
+ *
919
+ * What this is worth: a hop measured 367 microseconds per thousand requests on the machine this was
920
+ * written on, and the parser prologue it reaches measured 38. Ten to one, for a layer that had
921
+ * nothing to do.
922
+ *
923
+ * @param {any} route
924
+ * @param {any} req
925
+ * @returns {boolean}
926
+ */
927
+ function stepsOver(route, req) {
928
+ if (route.bodyParserOnly !== true || req._hasBodyHeaders === true) {
929
+ return false;
930
+ }
931
+ if (BODY_METHODS.has(req.method)) {
932
+ return false;
933
+ }
934
+ // an application can add its own. Read once and kept, which is what the parser behind this
935
+ // layer does with the same setting: asking on every request measured 17 microseconds per
936
+ // thousand, a third of what stepping over the layer saves
937
+ if (route.bodyMethods === undefined) {
938
+ route.bodyMethods = req.app.get("body methods") ?? null;
939
+ }
940
+ return route.bodyMethods === null || !route.bodyMethods.includes(req.method);
941
+ }
942
+
943
+ /**
944
+ * Whether a route could answer a request for this path, judged on the pattern it was compiled to.
945
+ * A literal answers only itself; anything with a parameter or a wildcard answers what its regex
946
+ * says. Used where the question is "would this earlier route have had its turn first".
947
+ *
948
+ * @param {any} route
949
+ * @param {string} path
950
+ * @returns {boolean}
951
+ */
952
+ function couldAnswer(route, path) {
953
+ if (route.pattern instanceof RegExp) {
954
+ return route.pattern.test(path);
955
+ }
956
+ return route.pattern === path;
957
+ }
958
+
959
+ /**
960
+ * Notes which application is current before a mounted one is entered, so that exact one comes back
961
+ * when it hands over.
962
+ *
963
+ * Only an application takes it back. Express restores req.app by putting the request prototype
964
+ * back, and it wraps a mounted application to do that only in Application#use: hang one off a plain
965
+ * Router and nothing restores it, so whatever runs afterwards still reads the settings of the
966
+ * application that was entered. Restoring regardless made a later res.send answer with the outer
967
+ * application's etag setting where express answers with the inner.
968
+ *
969
+ * And what comes back is what was current, not the entered application's parent. Those differ the
970
+ * moment a sub-app is entered from inside another sub-app that a plain Router mounted: the outer
971
+ * one is still current, express puts that one back, and reaching for `.parent` skipped a level.
972
+ * A 404 from the top application then carried an ETag under `app.set("etag", false)`, because the
973
+ * settings answering were the inner application's. Found by fuzzing three levels of routers.
974
+ *
975
+ * The route is remembered alongside, so the pop can only ever take back what this same route put
976
+ * there: a mounted application that answers instead of handing over leaves its entry behind, and
977
+ * the request is over by then.
978
+ *
979
+ * @param {any} walk
980
+ * @param {any} route
981
+ * @param {any} req
982
+ */
983
+ function rememberApp(walk, route, req) {
984
+ if (walk.router._isApplication && route.callbacks[0]?.constructor.name === "Application") {
985
+ (req._appStack ??= []).push(route, req.app);
986
+ }
987
+ }
988
+
989
+ /**
990
+ * Puts back what rememberApp noted, if this is the route that noted it.
991
+ *
992
+ * @param {any} route
993
+ * @param {any} req
994
+ */
995
+ function restoreApp(route, req) {
996
+ const stack = req._appStack;
997
+ if (stack !== undefined && stack.length > 0 && stack[stack.length - 2] === route) {
998
+ const app = stack.pop();
999
+ stack.pop();
1000
+ useApp(req, app);
1001
+ }
1002
+ }
1003
+
1004
+ /**
1005
+ * @param {any} req
1006
+ * @param {any} app
1007
+ */
755
1008
  function useApp(req, app) {
756
1009
  req.app = app;
757
1010
  if (req.res) {
@@ -776,7 +1029,8 @@ function useApp(req, app) {
776
1029
  const methods = ["all", ...METHODS.filter((method) => method !== "GET").map((method) => method.toLowerCase())];
777
1030
  const supportedUwsMethods = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]);
778
1031
 
779
- const regExParam = /:(\w+)/g;
1032
+ // the same name rule patternToRegex reads, so a unicode name is found here too
1033
+ const regExParam = /:([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/gu;
780
1034
 
781
1035
  // Internals here are _underscore and not #private: a callable router is a function with the
782
1036
  // router's properties copied onto it, and a # field cannot be copied, so #routes would throw
@@ -818,10 +1072,24 @@ function callablePrototypeFor(classPrototype) {
818
1072
  * The default error page, which is the one Express produces: the stack in a pre, and nothing else.
819
1073
  * What reaches it has already been redacted when the environment calls for it.
820
1074
  *
1075
+ * The text is escaped, which is not decoration. An error message can carry anything a client sent,
1076
+ * a path or a header among them, and writing it into the page unescaped put whatever it held into
1077
+ * the markup. The Content-Security-Policy on this response stops a script there from running, but
1078
+ * a policy is a second line and not the first. finalhandler escapes and then puts the line breaks
1079
+ * and the indentation back as markup, and this reads the same as what it produces.
1080
+ *
821
1081
  * @param {any} err
822
1082
  * @returns {string}
823
1083
  */
824
1084
  function generateErrorPageHtml(err) {
1085
+ const text = String(err?.stack ?? err)
1086
+ .replace(/&/g, "&amp;")
1087
+ .replace(/</g, "&lt;")
1088
+ .replace(/>/g, "&gt;")
1089
+ .replace(/"/g, "&quot;")
1090
+ .replace(/'/g, "&#39;")
1091
+ .replace(/\n/g, "<br>")
1092
+ .replace(/ {2}/g, " &nbsp;");
825
1093
  return (
826
1094
  `<!DOCTYPE html>\n` +
827
1095
  `<html lang="en">\n` +
@@ -830,7 +1098,7 @@ function generateErrorPageHtml(err) {
830
1098
  `<title>Error</title>\n` +
831
1099
  `</head>\n` +
832
1100
  `<body>\n` +
833
- `<pre>${err?.stack ?? err}</pre>\n` +
1101
+ `<pre>${text}</pre>\n` +
834
1102
  `</body>\n` +
835
1103
  `</html>\n`
836
1104
  );
@@ -860,6 +1128,36 @@ module.exports = class Router extends EventEmitter {
860
1128
  */
861
1129
  uwsApp;
862
1130
 
1131
+ /**
1132
+ * Whether an unset routing flag reads on through the mount parent. Only an application does,
1133
+ * because express chains a mounted app's settings onto its parent's; a plain Router keeps
1134
+ * whatever its options said and nothing else.
1135
+ *
1136
+ * @type {boolean}
1137
+ */
1138
+ _inheritsSettings = false;
1139
+
1140
+ /**
1141
+ * Whether this is an application rather than a plain router. Read on the hop out of a mount,
1142
+ * where only an application takes req.app back, and a field rather than a name comparison
1143
+ * because that sits on the dispatch path.
1144
+ *
1145
+ * @type {boolean}
1146
+ */
1147
+ _isApplication = false;
1148
+
1149
+ /**
1150
+ * The two routing flags once read, undefined until then. Express passes caseSensitive and
1151
+ * strict in when it builds a router and never looks at them again, so they are frozen here at
1152
+ * the first read rather than resolved per request.
1153
+ *
1154
+ * @type {boolean|undefined}
1155
+ */
1156
+ _strictFlag;
1157
+
1158
+ /** @type {boolean|undefined} */
1159
+ _caseFlag;
1160
+
863
1161
  /**
864
1162
  * @param {object} [settings] router options. caseSensitive and strict are accepted under the
865
1163
  * names Express's Router takes, and stored under the setting names the rest of the code reads
@@ -992,6 +1290,53 @@ module.exports = class Router extends EventEmitter {
992
1290
  return this.createRoute("GET", path, this, ...callbacks);
993
1291
  }
994
1292
 
1293
+ /**
1294
+ * A routing flag, read once and kept. Express builds a router's matcher the first time the
1295
+ * router is needed and hands it caseSensitive and strict there, so a mount that happens after
1296
+ * that, or an app.set() that happens after that, cannot change how this router matches. Asking
1297
+ * per request instead would let a strict application make every router mounted on it strict,
1298
+ * which express does not do.
1299
+ *
1300
+ * @param {string} key the setting name
1301
+ * @returns {boolean}
1302
+ */
1303
+ _routingFlag(key) {
1304
+ const own = this.settings[key];
1305
+ if (typeof own !== "undefined") {
1306
+ return Boolean(own);
1307
+ }
1308
+ return this._inheritsSettings && this.parent ? Boolean(this.parent.get(key)) : false;
1309
+ }
1310
+
1311
+ /**
1312
+ * Reads both flags at once, the first time either is wanted, because express reads both at
1313
+ * once too: it passes them together to the router it builds. Freezing them apart would let a
1314
+ * router end up strict from the moment before a mount and case sensitive from the moment
1315
+ * after it, which is a state express can never be in.
1316
+ */
1317
+ _freezeRoutingFlags() {
1318
+ if (this._strictFlag === undefined) {
1319
+ this._strictFlag = this._routingFlag("strict routing");
1320
+ this._caseFlag = this._routingFlag("case sensitive routing");
1321
+ }
1322
+ }
1323
+
1324
+ /**
1325
+ * @returns {boolean} whether this router tells /things from /things/
1326
+ */
1327
+ _strictRouting() {
1328
+ this._freezeRoutingFlags();
1329
+ return /** @type {boolean} */ (this._strictFlag);
1330
+ }
1331
+
1332
+ /**
1333
+ * @returns {boolean} whether this router tells /Things from /things
1334
+ */
1335
+ _caseSensitive() {
1336
+ this._freezeRoutingFlags();
1337
+ return /** @type {boolean} */ (this._caseFlag);
1338
+ }
1339
+
995
1340
  /**
996
1341
  * The pattern matching everything the mounts on this request have consumed so far, which is
997
1342
  * what a nested router strips off the path before matching against it. Cached per stack, since
@@ -1022,7 +1367,12 @@ module.exports = class Router extends EventEmitter {
1022
1367
  const stackPattern = fullStack.includes(":")
1023
1368
  ? fullStack.replace(/(\\?):(\w+)/g, (whole, escaped, name, at) => (escaped ? whole : ":m" + at))
1024
1369
  : fullStack;
1025
- fullMountpath = patternToRegex(stackPattern, true, Boolean(this.get("case sensitive routing")));
1370
+ // insensitive whatever this router says, because this only finds again a prefix that
1371
+ // has already been accepted, by the routers that own those mounts and under their
1372
+ // rules. A case sensitive router mounted on an insensitive app is reached as /LIST
1373
+ // while it is registered as /list, and compiling this one its way left the prefix in
1374
+ // place and every parameter below it unread
1375
+ fullMountpath = patternToRegex(stackPattern, true, false);
1026
1376
  this._mountpathCache.set(fullStack, fullMountpath);
1027
1377
  }
1028
1378
  return fullMountpath;
@@ -1038,18 +1388,10 @@ module.exports = class Router extends EventEmitter {
1038
1388
  * @returns {boolean}
1039
1389
  */
1040
1390
  _pathMatches(route, req) {
1391
+ // the path as it arrived, mount prefixes aside: whether a trailing slash is allowed is
1392
+ // written into the pattern, where express writes it too
1041
1393
  let path = req._opPath;
1042
1394
  let pattern = route.pattern;
1043
-
1044
- if (route.userRegexp) {
1045
- // a RegExp the application wrote is matched against the path as it arrived: express
1046
- // relaxes a trailing slash only for the paths it compiled itself
1047
- if (req.endsWithSlash && !path.endsWith("/")) {
1048
- path += "/";
1049
- }
1050
- } else if (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
1051
- path = path.slice(0, -1);
1052
- }
1053
1395
  // the line above turns the root path into the empty string, which no pattern is written
1054
1396
  // against. A regex route was tested against it and app.get("*path") answered every request
1055
1397
  // but "/"
@@ -1061,11 +1403,22 @@ module.exports = class Router extends EventEmitter {
1061
1403
  if (pattern === "/*") {
1062
1404
  return true;
1063
1405
  }
1064
- if (!this.get("case sensitive routing")) {
1406
+ if (!this._caseSensitive()) {
1065
1407
  path = path.toLowerCase();
1066
1408
  pattern = pattern.toLowerCase();
1067
1409
  }
1068
- return pattern === path;
1410
+ if (pattern === path) {
1411
+ return true;
1412
+ }
1413
+ // a literal path is compared as text rather than compiled, so the trailing slash a
1414
+ // pattern would have carried as "/?" is allowed here instead. The registered path has
1415
+ // had its own taken off already, unless it is the root
1416
+ return (
1417
+ !this._strictRouting() &&
1418
+ path.length === pattern.length + 1 &&
1419
+ path.charCodeAt(path.length - 1) === 0x2f &&
1420
+ path.startsWith(pattern)
1421
+ );
1069
1422
  }
1070
1423
  if (pattern === EMPTY_REGEX) {
1071
1424
  return true;
@@ -1105,12 +1458,15 @@ module.exports = class Router extends EventEmitter {
1105
1458
  // a mount always drops it, strict routing or not: strictness is about the end of a
1106
1459
  // path, and a mount has none. Express registers its use layers with strict off
1107
1460
  if (
1108
- (method === "USE" || !this.get("strict routing")) &&
1461
+ (method === "USE" || !this._strictRouting()) &&
1109
1462
  typeof path === "string" &&
1110
1463
  path.endsWith("/") &&
1111
1464
  path !== "/"
1112
1465
  ) {
1113
- path = path.slice(0, -1);
1466
+ // every one of them, not the last: express loosens with /\/+$/, so a route written
1467
+ // "/test//" is registered as "/test" and answers "/test" and "/test/" but not the
1468
+ // path it was written as
1469
+ path = path.replace(/\/+$/, "");
1114
1470
  }
1115
1471
  if (path === "*") {
1116
1472
  path = "/{*splat}";
@@ -1120,9 +1476,7 @@ module.exports = class Router extends EventEmitter {
1120
1476
  path,
1121
1477
  pattern:
1122
1478
  method === "USE" || needsConversionToRegex(path)
1123
- ? // Boolean, not the raw value: an unset setting reads undefined, which a
1124
- // default parameter would silently turn back into case-sensitive
1125
- patternToRegex(path, method === "USE", Boolean(this.get("case sensitive routing")))
1479
+ ? patternToRegex(path, method === "USE", this._caseSensitive(), this._strictRouting())
1126
1480
  : path,
1127
1481
  callbacks,
1128
1482
  // instanceof walks a prototype chain and length is a property load, and both used
@@ -1134,12 +1488,23 @@ module.exports = class Router extends EventEmitter {
1134
1488
  ? CALLBACK_ERROR
1135
1489
  : CALLBACK_PLAIN
1136
1490
  ),
1491
+ // A body parser, and nothing else: they carry the mark that says their prologue
1492
+ // leaves a request that declared no body alone. Reaching one costs a hop, and the
1493
+ // hop measures ten times what the prologue does, so a request that provably gets
1494
+ // nothing out of it steps over the whole layer. See stepsOver
1495
+ bodyParserOnly: method === "USE" && callbacks.length === 1 && callbacks[0][kGetSafe] === true,
1496
+ // the "body methods" setting as it stood the first time this layer was reached,
1497
+ // kept the way the parser behind it keeps it. undefined until then
1498
+ bodyMethods: undefined,
1137
1499
  // a mount written as a RegExp matches a piece of path that is not known until a
1138
1500
  // request comes in, so its stack entry cannot be the path itself
1139
1501
  regexMount: method === "USE" && path instanceof RegExp,
1140
1502
  // written by the application, so express matches it as it stands
1141
1503
  userRegexp: path instanceof RegExp,
1142
1504
  routeKey: routeKey++,
1505
+ // which app.route() this came from, when it came from one, so the routes it built
1506
+ // count as one route where an error is concerned. undefined for every other route
1507
+ group: this._pendingGroup,
1143
1508
  // the router this was registered on. Ordinary dispatch is done by that router, so
1144
1509
  // it could ask itself, but an optimized chain is walked by the app whatever it
1145
1510
  // contains, and param() callbacks belong to the router that declared them
@@ -1197,8 +1562,12 @@ module.exports = class Router extends EventEmitter {
1197
1562
  // so the text comparisons below run on the folded form. µWS itself still matches bytes:
1198
1563
  // a request in the registered case takes the chain, any other case takes the fallback,
1199
1564
  // and both answer as express would as long as the chain agrees with registration order
1200
- const caseSensitive = Boolean(this.get("case sensitive routing"));
1565
+ const caseSensitive = this._caseSensitive();
1201
1566
  const routePathFolded = caseSensitive || typeof route.path !== "string" ? route.path : route.path.toLowerCase();
1567
+ // whether this route answers only the path as written, or the one with a trailing slash too
1568
+ const strictHere = (route.owner ?? this)._strictRouting();
1569
+ /** @type {string[]|null} earlier literals a case variant could smuggle a request past */
1570
+ let caseGuards = null;
1202
1571
 
1203
1572
  for (let i = 0; i < routes.length; i++) {
1204
1573
  const r = routes[i];
@@ -1212,6 +1581,18 @@ module.exports = class Router extends EventEmitter {
1212
1581
  if (!r.all && r.method !== route.method) {
1213
1582
  // check if the methods are compatible (GET and HEAD)
1214
1583
  if (!(r.method === "HEAD" && route.method === "GET")) {
1584
+ // A mount is registered ALL, because what lives under it can answer any
1585
+ // method, and this chain is computed once for all of them. So an earlier
1586
+ // route of some other method is not irrelevant here the way it is for a
1587
+ // plain route: it belongs in the chain of the leaves that share its method
1588
+ // and in no other, and one chain cannot say that. µWS would then jump
1589
+ // straight to a leaf and answer as though the earlier route did not exist,
1590
+ // which is what let a literal route inside a mounted router beat a parameter
1591
+ // route written before the mount. Leave the mount to ordinary dispatch,
1592
+ // where express's own order is what decides.
1593
+ if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
1594
+ return false;
1595
+ }
1215
1596
  continue;
1216
1597
  }
1217
1598
  }
@@ -1241,9 +1622,14 @@ module.exports = class Router extends EventEmitter {
1241
1622
  }
1242
1623
 
1243
1624
  // check if the paths match. A route with parameters is excluded from the text test:
1244
- // its literal ":name" text would let an earlier regex in on requests it never matches
1625
+ // its literal ":name" text would let an earlier regex in on requests it never matches.
1626
+ // Both spellings of this route's path are tried, because without strict routing it
1627
+ // answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
1628
+ // still an earlier pattern that answers a request this registration would take.
1245
1629
  if (
1246
- (r.pattern instanceof RegExp && (!withParams || r.use) && r.pattern.test(route.path)) ||
1630
+ (r.pattern instanceof RegExp &&
1631
+ (!withParams || r.use) &&
1632
+ (r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
1247
1633
  (typeof r.pattern === "string" &&
1248
1634
  (r.pattern === route.path ||
1249
1635
  (!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
@@ -1281,18 +1667,26 @@ module.exports = class Router extends EventEmitter {
1281
1667
  continue;
1282
1668
  }
1283
1669
  // otherwise the two overlap only where µWS itself hands the request to the earlier,
1284
- // more specific registration, so this chain never sees those paths. That argument is
1285
- // about bytes, so under insensitive routing it only holds when no literal hides
1286
- // behind a case difference
1670
+ // more specific registration, so this chain never sees those paths
1287
1671
  if (
1288
1672
  !r.optimizedPath ||
1289
1673
  !uwsPrefersEarlier(r.path, route.path) ||
1290
- (!caseSensitive && (r.path !== rPathFolded || route.path !== routePathFolded))
1674
+ (!caseSensitive && route.path !== routePathFolded)
1291
1675
  ) {
1292
1676
  return false;
1293
1677
  }
1678
+ // that argument is about bytes. Under insensitive routing "/POSTS" byte-matches no
1679
+ // registration of "/posts", so µWS hands it here instead, where this chain would
1680
+ // answer as if the earlier route did not exist. The literal is remembered so the
1681
+ // registration can send those requests to the generic router, which is the only place
1682
+ // express's own order can decide; a path with no letter in it has no other case to
1683
+ // arrive in and needs no guard
1684
+ if (!caseSensitive && HAS_LETTER.test(r.path)) {
1685
+ (caseGuards ??= []).push(r.path);
1686
+ }
1294
1687
  }
1295
1688
  optimizedPath.push(route);
1689
+ route._caseGuards = caseGuards;
1296
1690
 
1297
1691
  return optimizedPath;
1298
1692
  }
@@ -1324,8 +1718,10 @@ module.exports = class Router extends EventEmitter {
1324
1718
  ) {
1325
1719
  let pathToMount = router._optimizeRoute(route, router._routes);
1326
1720
  if (!pathToMount) {
1721
+ route._whyGeneric = "something before it in the same router overlaps its paths";
1327
1722
  continue;
1328
1723
  }
1724
+ route._walkedInto = true;
1329
1725
  pathToMount = pathToMount.slice(0, -1);
1330
1726
  walk(route.callbacks[0], pathPrefix + route.path, [
1331
1727
  ...chainPrefix,
@@ -1342,6 +1738,13 @@ module.exports = class Router extends EventEmitter {
1342
1738
  : undefined
1343
1739
  }
1344
1740
  ]);
1741
+ } else {
1742
+ // said once here rather than at each condition above: a mount is walked into
1743
+ // only when µWS can match its path on its own and it carries exactly one
1744
+ // router, and those are the two things worth telling anyone about
1745
+ route._whyGeneric = !(route.callbacks.length === 1 && route.callbacks[0] instanceof Router)
1746
+ ? "it is middleware rather than a single mounted router"
1747
+ : "µWS cannot match this mount path on its own";
1345
1748
  }
1346
1749
  // µWS picks by specificity and Express by registration order, so the chain
1347
1750
  // computed for whichever route µWS lands on runs everything that could have
@@ -1356,6 +1759,7 @@ module.exports = class Router extends EventEmitter {
1356
1759
  ) {
1357
1760
  const leafPath = router._optimizeRoute(route, router._routes);
1358
1761
  if (!leafPath) {
1762
+ route._whyGeneric = "something before it in the same router overlaps its paths";
1359
1763
  continue;
1360
1764
  }
1361
1765
  // param route earlier in the same router would steal this static path
@@ -1368,6 +1772,7 @@ module.exports = class Router extends EventEmitter {
1368
1772
  shadow.path !== route.path &&
1369
1773
  shadow.pattern instanceof RegExp
1370
1774
  ) {
1775
+ route._whyGeneric = `the parameter route ${shadow.path} is written before it`;
1371
1776
  continue;
1372
1777
  }
1373
1778
  }
@@ -1382,6 +1787,11 @@ module.exports = class Router extends EventEmitter {
1382
1787
  pattern: pathPrefix + route.path,
1383
1788
  optimizedRouter: true
1384
1789
  };
1790
+ if (route._caseGuards) {
1791
+ // compared against the whole path µWS matched, so they carry the mount
1792
+ // prefix, folded along with the rest of it
1793
+ registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
1794
+ }
1385
1795
  this._registerUwsRoute(registered, chain);
1386
1796
  // the chain holds the original object, so the request-time guard has to
1387
1797
  // find the computed fields there, or a mounted param route extracts its
@@ -1389,9 +1799,19 @@ module.exports = class Router extends EventEmitter {
1389
1799
  // adds no parameter of its own
1390
1800
  route.optimizedParams = registered.optimizedParams;
1391
1801
  route.optimizedPath = registered.optimizedPath;
1802
+ // and what was decided about it, for the same reason: the copy is thrown
1803
+ // away and the profile reads the route the application actually holds
1804
+ route._native = registered._native;
1392
1805
  } else {
1393
1806
  this._registerUwsRoute(route, chain);
1394
1807
  }
1808
+ } else if (!supportedUwsMethods.has(route.method)) {
1809
+ route._whyGeneric = `µWS does not serve ${route.method}`;
1810
+ } else if (canBeOptimizedWithParams(route.path)) {
1811
+ // eligible but for the overlap test, which only applies inside a mount
1812
+ route._whyGeneric = "a route after it in the same mounted router could answer the same paths";
1813
+ } else {
1814
+ route._whyGeneric = "µWS cannot match this path on its own";
1395
1815
  }
1396
1816
  }
1397
1817
  };
@@ -1445,7 +1865,7 @@ module.exports = class Router extends EventEmitter {
1445
1865
  */
1446
1866
  _isFollowedByAnOverlap(route, routes) {
1447
1867
  // folded under insensitive routing, where a case variant answers the same requests
1448
- const caseSensitive = Boolean(this.get("case sensitive routing"));
1868
+ const caseSensitive = this._caseSensitive();
1449
1869
  const routePath = caseSensitive ? route.path : route.path.toLowerCase();
1450
1870
  for (let i = routes.length - 1; i >= 0; i--) {
1451
1871
  const later = routes[i];
@@ -1487,6 +1907,14 @@ module.exports = class Router extends EventEmitter {
1487
1907
  if (route.path.includes(":")) {
1488
1908
  route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
1489
1909
  }
1910
+ // null for almost every route: only a parameter route with an earlier literal that a case
1911
+ // variant could slip past carries one, see _optimizeRoute. Built once here, and matched
1912
+ // insensitively, since that is the folding the guard exists for
1913
+ const caseGuards = route._caseGuards
1914
+ ? route._caseGuards.map((p) =>
1915
+ needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
1916
+ )
1917
+ : null;
1490
1918
  const makeHandler = (chain, preset, skips) => {
1491
1919
  // the mutable object a granted skip lives on, so a middleware arriving after
1492
1920
  // listen can take it back: a literal registration's preset doubles as it, and a
@@ -1508,6 +1936,13 @@ module.exports = class Router extends EventEmitter {
1508
1936
  // and this one never did. nativeDone and nativeFail defer their epilogues to a
1509
1937
  // microtask, which is where the await used to resume, so the visible order holds
1510
1938
  return (res, req) => {
1939
+ // a request that is an earlier literal in another case: express answers it with
1940
+ // that route, and the chain here does not contain it, so the generic router takes
1941
+ // this one
1942
+ if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
1943
+ // an application is what registers native routes, and only it serves
1944
+ return /** @type {any} */ (this)._serveGeneric(res, req);
1945
+ }
1511
1946
  const request = this.handleRequest(res, req, preset, skipHolder);
1512
1947
  const response = request.res;
1513
1948
  if (optimizedParams) {
@@ -1548,7 +1983,7 @@ module.exports = class Router extends EventEmitter {
1548
1983
  // the route's own router decides, not the app running the registration: a router created
1549
1984
  // with { strict: true } and mounted on an app without it does not answer /things/, and
1550
1985
  // registering that path here is the only way it could
1551
- const strictHere = Boolean((route.owner ?? this).get("strict routing"));
1986
+ const strictHere = (route.owner ?? this)._strictRouting();
1552
1987
 
1553
1988
  // Whether requests served by this registration may skip the header copy: GET and its
1554
1989
  // HEAD twins only, the app must not compute etags (send would consult freshness
@@ -1574,7 +2009,7 @@ module.exports = class Router extends EventEmitter {
1574
2009
  }
1575
2010
  // remembered so a middleware or setting arriving after listen can take the skips back
1576
2011
  const makePreset = (path, method, skips) => {
1577
- const preset = nativePreset(path, method, strictHere);
2012
+ const preset = nativePreset(path, method);
1578
2013
  if (skips.skipHeaders || skips.skipQuery) {
1579
2014
  preset.skipHeaders = skips.skipHeaders;
1580
2015
  preset.skipQuery = skips.skipQuery;
@@ -1601,6 +2036,9 @@ module.exports = class Router extends EventEmitter {
1601
2036
  route.callbacks.length === 1 && // must not have multiple callbacks
1602
2037
  typeof route.callbacks[0] === "function" && // must be a function
1603
2038
  route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
2039
+ // a declarative response is answered by µWS itself, so no javascript runs and the case
2040
+ // guard could not: a route that needs one has to stay an ordinary handler
2041
+ caseGuards === null &&
1604
2042
  !resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
1605
2043
  this.get("declarative responses") // must have declarative responses enabled
1606
2044
  ) {
@@ -1612,6 +2050,18 @@ module.exports = class Router extends EventEmitter {
1612
2050
  replacedPath = route.path.replace(regExParam, ":x");
1613
2051
  }
1614
2052
 
2053
+ // what listen() settled about this route, kept so `npx fulmine profile` can print it rather
2054
+ // than making anyone read the source or instrument it. Written once, during compilation,
2055
+ // so no request pays for it
2056
+ route._native = {
2057
+ path: replacedPath,
2058
+ declarative: fn !== jsFn,
2059
+ skipHeaders: getSkips.skipHeaders === true,
2060
+ skipQuery: getSkips.skipQuery === true,
2061
+ ahead: optimizedPath.length - 1,
2062
+ guards: caseGuards ? caseGuards.length : 0
2063
+ };
2064
+
1615
2065
  this.uwsApp[method](replacedPath, fn);
1616
2066
  if (!strictHere && route.path[route.path.length - 1] !== "/") {
1617
2067
  // a declarative response answers the twin as itself; a preset handler cannot be
@@ -1739,10 +2189,10 @@ module.exports = class Router extends EventEmitter {
1739
2189
  // asking for each name in turn rather than walking the groups object, which is a
1740
2190
  // null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
1741
2191
  // worked out when the pattern was compiled instead of searching an array for it
1742
- const { paramNames, isWildcard } = meta;
2192
+ const { paramNames, outputNames, isWildcard } = meta;
1743
2193
  for (let i = 0, len = paramNames.length; i < len; i++) {
1744
- const name = paramNames[i];
1745
- const value = groups[name];
2194
+ const name = outputNames[i];
2195
+ const value = groups[paramNames[i]];
1746
2196
  // an optional group that did not match is absent in v5, not present as undefined
1747
2197
  if (value === undefined) {
1748
2198
  continue;
@@ -1776,36 +2226,29 @@ module.exports = class Router extends EventEmitter {
1776
2226
  req.params[name] = decodeParam(req.optimizedParams[name]);
1777
2227
  }
1778
2228
  } catch (err) {
1779
- req._error = err;
1780
- req._errorKey = route.routeKey;
2229
+ raiseDecodeFailure(req, route, err);
1781
2230
  return "route";
1782
2231
  }
1783
2232
  } else if (route.complex) {
1784
- let path = req._originalPath;
1785
- if (req._stack !== null && req._stack.length > 0) {
1786
- const fullMountpath = this.getFullMountpath(req);
1787
- if (fullMountpath !== EMPTY_REGEX) {
1788
- path = path.replace(fullMountpath, "");
1789
- }
1790
- }
2233
+ // the path with the mounts taken off, which is what _opPath is
2234
+ const path = req._opPath;
1791
2235
  try {
1792
2236
  req.params = this._extractParams(route.pattern, path);
1793
2237
  } catch (err) {
1794
2238
  // a parameter that will not decode. Express throws out of the match and lets the
1795
2239
  // error reach the error handler, which answers 400, so the route is skipped rather
1796
2240
  // than run with a value nobody can read.
1797
- req._error = err;
1798
- req._errorKey = route.routeKey;
2241
+ raiseDecodeFailure(req, route, err);
1799
2242
  return "route";
1800
2243
  }
1801
- if (req._paramStack !== null && req._paramStack.length > 0) {
2244
+ if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
1802
2245
  req.params = mergeParams(req.params, req._paramStack);
1803
2246
  }
1804
2247
  } else {
1805
2248
  // express 5 gives every matched route null-prototype params; only a pathless
1806
2249
  // middleware layer keeps the plain object, as its router hands one to fast_slash
1807
2250
  req.params = route.use && route.path === "" ? {} : Object.create(null);
1808
- if (req._paramStack !== null && req._paramStack.length > 0) {
2251
+ if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
1809
2252
  req.params = mergeParams(req.params, req._paramStack);
1810
2253
  }
1811
2254
  }
@@ -1819,6 +2262,27 @@ module.exports = class Router extends EventEmitter {
1819
2262
  return true;
1820
2263
  }
1821
2264
 
2265
+ /**
2266
+ * Whether this route's parameters carry a percent escape that will not decode. Asked only of a
2267
+ * route whose path matched and whose method did not, which express still decodes: the 400 it
2268
+ * answers there is what this reproduces.
2269
+ *
2270
+ * @param {any} route
2271
+ * @param {any} req
2272
+ * @returns {boolean}
2273
+ */
2274
+ _paramsFailToDecode(route, req) {
2275
+ if (!route.complex) {
2276
+ return false;
2277
+ }
2278
+ try {
2279
+ this._extractParams(route.pattern, req._opPath);
2280
+ return false;
2281
+ } catch {
2282
+ return true;
2283
+ }
2284
+ }
2285
+
1822
2286
  /**
1823
2287
  * Runs the app.param() callbacks for the parameters this route matched, and says whether the
1824
2288
  * route may run.
@@ -1864,6 +2328,7 @@ module.exports = class Router extends EventEmitter {
1864
2328
  if (err !== "route") {
1865
2329
  req._error = err;
1866
2330
  req._errorKey = route.routeKey;
2331
+ req._errorGroup = route.group;
1867
2332
  }
1868
2333
  // the route is skipped either way: an error carries on to the error handlers
1869
2334
  return resolve("route");
@@ -2037,15 +2502,22 @@ module.exports = class Router extends EventEmitter {
2037
2502
  * @returns {object} an object with one method per HTTP verb, each returning it again
2038
2503
  */
2039
2504
  route(path) {
2505
+ // everything hung off one app.route() shares this, which is what makes them one route as
2506
+ // far as an error is concerned, see errorHop
2507
+ const group = ++routeGroups;
2040
2508
  const fns = new NullObject();
2041
- for (const method of methods) {
2042
- fns[method] = (...callbacks) => {
2509
+ const inGroup = (method, callbacks) => {
2510
+ this._pendingGroup = group;
2511
+ try {
2043
2512
  return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
2044
- };
2045
- }
2046
- fns.get = (...callbacks) => {
2047
- return this.createRoute("GET", path, /** @type {any} */ (fns), ...callbacks);
2513
+ } finally {
2514
+ this._pendingGroup = undefined;
2515
+ }
2048
2516
  };
2517
+ for (const method of methods) {
2518
+ fns[method] = (...callbacks) => inGroup(method, callbacks);
2519
+ }
2520
+ fns.get = (...callbacks) => inGroup("GET", callbacks);
2049
2521
  return fns;
2050
2522
  }
2051
2523
 
@@ -2074,14 +2546,15 @@ module.exports = class Router extends EventEmitter {
2074
2546
  *
2075
2547
  * @param {any} request
2076
2548
  * @param {any} response
2549
+ * @param {Set<string>} methods the verbs the answering router knows, which are its own
2077
2550
  */
2078
- _sendOptionsReply(request, response) {
2551
+ _sendOptionsReply(request, response, methods) {
2079
2552
  if (response._headWritten) {
2080
2553
  throw new Error("Cannot set headers after they are sent to the client");
2081
2554
  }
2082
2555
  // Express 5 sorts the methods and joins them with ", ", so the header reads the same
2083
2556
  // regardless of the order the routes happened to be registered in
2084
- const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
2557
+ const allowedMethods = Array.from(methods).sort().join(", ");
2085
2558
  response.setHeader("Allow", allowedMethods);
2086
2559
  // the router package answers this one itself, with a plain-text body, the nosniff
2087
2560
  // header and end() rather than send(), so no ETag comes with it
@@ -2104,7 +2577,7 @@ module.exports = class Router extends EventEmitter {
2104
2577
  }
2105
2578
  if (request._isOptions && request._matchedMethods.size > 0) {
2106
2579
  try {
2107
- this._sendOptionsReply(request, response);
2580
+ this._sendOptionsReply(request, response, request._matchedMethods);
2108
2581
  } catch (err) {
2109
2582
  // a head already written: the error answers instead, as express's does
2110
2583
  this._handleError(err, null, request, response);