fulmine.js 5.2.0 → 5.4.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/NOTICE +29 -2
- package/README.md +176 -63
- package/package.json +11 -2
- package/src/application.js +67 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +28 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +67 -6
- package/src/node-shim.js +16 -3
- package/src/options.d.ts +16 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +365 -60
- package/src/response.js +172 -12
- package/src/router.js +767 -177
- package/src/types.d.ts +16 -0
- package/src/usage.js +16 -0
- package/src/utils.js +272 -37
- package/src/view.js +2 -0
- package/src/websocket.js +16 -0
- package/src/worker.js +2 -0
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
|
-
|
|
118
|
-
|
|
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.
|
|
138
|
-
req
|
|
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.
|
|
196
|
-
req
|
|
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,29 @@ 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
|
-
|
|
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 (
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
// the same step when the route has one callback, and then it has to be the same object:
|
|
337
|
+
// express hands res.format's handlers the next its own layer received, and its test asserts
|
|
338
|
+
// that identity. With more than one callback the two differ for real, and what express
|
|
339
|
+
// hands over is the one that leaves the route
|
|
340
|
+
req._leaveRoute = route.callbacks.length > 1 ? this.leaveRoute : this.next;
|
|
250
341
|
if (continueRoute === "route") {
|
|
251
342
|
this.step("route");
|
|
252
343
|
} else if (continueRoute) {
|
|
@@ -266,7 +357,15 @@ class Walk {
|
|
|
266
357
|
errorHop(kind, callback) {
|
|
267
358
|
const req = this.req;
|
|
268
359
|
const route = this.route;
|
|
269
|
-
|
|
360
|
+
// A four argument handler written inside a route only ever sees what that route raised:
|
|
361
|
+
// express skips a route layer entirely while an error is in flight, so an error from a
|
|
362
|
+
// middleware before it, or out of a mount, walks past to the router's own error handlers.
|
|
363
|
+
// Middleware error handlers keep the ordinary rule, which is that they catch what was
|
|
364
|
+
// raised before them.
|
|
365
|
+
const reachable = route.use
|
|
366
|
+
? route.routeKey >= req._errorKey
|
|
367
|
+
: route.routeKey === req._errorKey || (route.group !== undefined && route.group === req._errorGroup);
|
|
368
|
+
if (req._error && kind === CALLBACK_ERROR && reachable) {
|
|
270
369
|
const out = this.router._handleError(req._error, callback, req, this.res);
|
|
271
370
|
if (out instanceof Promise) {
|
|
272
371
|
// an error handler's rejected promise moves on to the next error handler, and
|
|
@@ -274,6 +373,7 @@ class Walk {
|
|
|
274
373
|
out.catch((err) => {
|
|
275
374
|
req._error = err || new Error("Rejected promise");
|
|
276
375
|
req._errorKey = route.routeKey;
|
|
376
|
+
req._errorGroup = route.group;
|
|
277
377
|
return this.step(undefined);
|
|
278
378
|
});
|
|
279
379
|
}
|
|
@@ -301,45 +401,20 @@ class Walk {
|
|
|
301
401
|
if (req.url !== req._lastUrl) {
|
|
302
402
|
req._absorbUrlRewrite();
|
|
303
403
|
}
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
}
|
|
404
|
+
req._consumed -= req._stack.pop();
|
|
405
|
+
setMountedPath(req);
|
|
406
|
+
restoreApp(route, req);
|
|
337
407
|
}
|
|
338
408
|
if (thingamabob === "router") {
|
|
339
409
|
if (this.skipCheck) {
|
|
340
410
|
// on a compiled chain, leaving the router is what running out of chain
|
|
341
|
-
// already means: ordinary routing takes over after the mount
|
|
342
|
-
|
|
411
|
+
// already means: ordinary routing takes over after the mount. With no
|
|
412
|
+
// mount in the chain the router being left is the app's own, and nothing
|
|
413
|
+
// of it may run afterwards, not even a middleware registered later
|
|
414
|
+
if (this.skipUntil?.keepMount) {
|
|
415
|
+
return this.dispatch(this.routes.length);
|
|
416
|
+
}
|
|
417
|
+
return this.resolve(false);
|
|
343
418
|
}
|
|
344
419
|
// out of this router entirely, so whoever mounted it carries on after the
|
|
345
420
|
// mount. The app's own walk has nobody after it, and answers 404
|
|
@@ -356,6 +431,7 @@ class Walk {
|
|
|
356
431
|
} else {
|
|
357
432
|
req._error = thingamabob;
|
|
358
433
|
req._errorKey = route.routeKey;
|
|
434
|
+
req._errorGroup = route.group;
|
|
359
435
|
}
|
|
360
436
|
}
|
|
361
437
|
const kind = route.callbackKinds[this.callbackIndex];
|
|
@@ -371,6 +447,7 @@ class Walk {
|
|
|
371
447
|
}
|
|
372
448
|
if (kind === CALLBACK_ROUTER) {
|
|
373
449
|
if (callback.constructor.name === "Application") {
|
|
450
|
+
rememberApp(this, route, req);
|
|
374
451
|
useApp(req, callback);
|
|
375
452
|
}
|
|
376
453
|
const pushedParams = callback.settings.mergeParams;
|
|
@@ -380,12 +457,12 @@ class Walk {
|
|
|
380
457
|
// express restores req.params when a router hands back, so what runs after the mount
|
|
381
458
|
// sees the params it had before it
|
|
382
459
|
const parentParams = req.params;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
) {
|
|
388
|
-
req.
|
|
460
|
+
// each router answers OPTIONS with the verbs it knows itself, so the one being entered
|
|
461
|
+
// starts its own list: express keeps that list per router, and a router that hands back
|
|
462
|
+
// without answering leaves the outer one's untouched
|
|
463
|
+
const parentMethods = req._matchedMethods;
|
|
464
|
+
if (parentMethods !== null) {
|
|
465
|
+
req._matchedMethods = new Set();
|
|
389
466
|
}
|
|
390
467
|
callback
|
|
391
468
|
._routeRequest(req, res, 0)
|
|
@@ -397,15 +474,25 @@ class Walk {
|
|
|
397
474
|
req.params = parentParams;
|
|
398
475
|
if (req._error) {
|
|
399
476
|
req._errorKey = route.routeKey;
|
|
477
|
+
req._errorGroup = route.group;
|
|
478
|
+
}
|
|
479
|
+
if (routed) {
|
|
480
|
+
if (parentMethods !== null) {
|
|
481
|
+
req._matchedMethods = parentMethods;
|
|
482
|
+
}
|
|
483
|
+
return this.resolve(true);
|
|
484
|
+
}
|
|
485
|
+
const childMethods = req._matchedMethods;
|
|
486
|
+
if (parentMethods !== null) {
|
|
487
|
+
req._matchedMethods = parentMethods;
|
|
400
488
|
}
|
|
401
|
-
if (
|
|
402
|
-
if (req._isOptions && req._matchedMethods.size) {
|
|
489
|
+
if (req._isOptions && childMethods.size) {
|
|
403
490
|
// OPTIONS routing is different, it stops in the router if matched.
|
|
404
491
|
// Express answers as the router hands back, so a throw while answering,
|
|
405
492
|
// a head already written being the way, walks on to later error handlers
|
|
406
493
|
if (!req._error) {
|
|
407
494
|
try {
|
|
408
|
-
router._sendOptionsReply(req, res);
|
|
495
|
+
router._sendOptionsReply(req, res, childMethods);
|
|
409
496
|
return this.resolve(true);
|
|
410
497
|
} catch (err) {
|
|
411
498
|
return this.step(err);
|
|
@@ -445,12 +532,14 @@ class Walk {
|
|
|
445
532
|
out.catch((err) => {
|
|
446
533
|
req._error = err || new Error("Rejected promise");
|
|
447
534
|
req._errorKey = route.routeKey;
|
|
535
|
+
req._errorGroup = route.group;
|
|
448
536
|
return this.step(undefined);
|
|
449
537
|
});
|
|
450
538
|
}
|
|
451
539
|
} catch (err) {
|
|
452
540
|
req._error = err;
|
|
453
541
|
req._errorKey = route.routeKey;
|
|
542
|
+
req._errorGroup = route.group;
|
|
454
543
|
return this.step(undefined);
|
|
455
544
|
}
|
|
456
545
|
}
|
|
@@ -513,22 +602,62 @@ function nativeFail(err) {
|
|
|
513
602
|
}
|
|
514
603
|
|
|
515
604
|
/**
|
|
516
|
-
*
|
|
517
|
-
*
|
|
605
|
+
* How much of the path a mount takes, which is what its own pattern matched and never more than
|
|
606
|
+
* there is. Exec runs on the same fixed-up path _pathMatches tested: a parent mount that consumed
|
|
518
607
|
* everything leaves "", where the pattern was matched against "/".
|
|
519
608
|
*
|
|
520
|
-
*
|
|
609
|
+
* Counting what each mount took, rather than rebuilding one pattern out of the whole stack and
|
|
610
|
+
* matching that against the original path, is the difference between a sum and a guess: a mount
|
|
611
|
+
* written as an optional group composes into a pattern the path no longer satisfies, and the
|
|
612
|
+
* prefix stayed on.
|
|
613
|
+
*
|
|
521
614
|
* @param {any} route
|
|
522
615
|
* @param {any} req
|
|
523
|
-
* @returns {
|
|
616
|
+
* @returns {number}
|
|
524
617
|
*/
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
if (
|
|
528
|
-
|
|
618
|
+
function mountPrefixLength(route, req) {
|
|
619
|
+
const path = req._opPath;
|
|
620
|
+
if (typeof route.pattern === "string") {
|
|
621
|
+
return route.pattern.length;
|
|
529
622
|
}
|
|
530
|
-
const matched = route.pattern.exec(
|
|
531
|
-
return matched ?
|
|
623
|
+
const matched = route.pattern.exec(path === "" ? "/" : path);
|
|
624
|
+
return matched ? Math.min(matched[0].length, path.length) : 0;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Writes the path the routes below a mount see: the original with what the mounts took off the
|
|
629
|
+
* front. The root reads as "/" rather than as nothing, which is how express hands it over.
|
|
630
|
+
*
|
|
631
|
+
* @param {any} req
|
|
632
|
+
*/
|
|
633
|
+
function setMountedPath(req) {
|
|
634
|
+
req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
|
|
635
|
+
req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
|
|
636
|
+
req.path = req._opPath === "" ? "/" : req._opPath;
|
|
637
|
+
req._lastUrl = req.url;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The route's own params merged with those of the mounts it sits under, in express's order: an
|
|
642
|
+
* outer mount first, the route's own last. Numbered captures do not overwrite each other, they
|
|
643
|
+
* shift, so a RegExp mount capturing one group leaves the route's own group numbered from one.
|
|
644
|
+
*
|
|
645
|
+
* @param {Record<string, any>} own what this route's own pattern captured
|
|
646
|
+
* @param {Record<string, any>[]} stack the mounts, outermost first
|
|
647
|
+
* @returns {Record<string, any>}
|
|
648
|
+
*/
|
|
649
|
+
/**
|
|
650
|
+
* Whether this route reads the parameters of the mounts above it, which is its own router asking
|
|
651
|
+
* for them. The stack holds what a mergeParams router captured on the way in, and a plain router
|
|
652
|
+
* mounted inside one must not read it: express asks each router in turn, not the outermost.
|
|
653
|
+
*
|
|
654
|
+
* @param {any} route
|
|
655
|
+
* @param {any} fallback the router dispatching, when the route names no owner
|
|
656
|
+
* @returns {boolean}
|
|
657
|
+
*/
|
|
658
|
+
function mergesParams(route, fallback) {
|
|
659
|
+
const owner = route.owner ?? fallback;
|
|
660
|
+
return Boolean(owner?.settings?.mergeParams);
|
|
532
661
|
}
|
|
533
662
|
|
|
534
663
|
/**
|
|
@@ -628,7 +757,7 @@ function adoptPlainRequest(req, router) {
|
|
|
628
757
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
629
758
|
req._originalPath = path;
|
|
630
759
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
631
|
-
req._opPath =
|
|
760
|
+
req._opPath = path;
|
|
632
761
|
req._lastUrl = req.url;
|
|
633
762
|
req._isOptions = req.method === "OPTIONS";
|
|
634
763
|
req._isHead = req.method === "HEAD";
|
|
@@ -636,7 +765,7 @@ function adoptPlainRequest(req, router) {
|
|
|
636
765
|
// null, not fresh arrays: the push sites materialize them on the first mount, and most
|
|
637
766
|
// requests never see one, same as the Request constructor
|
|
638
767
|
req._stack = null;
|
|
639
|
-
req.
|
|
768
|
+
req._consumed = 0;
|
|
640
769
|
req._paramStack = null;
|
|
641
770
|
req._matchedMethods = req._isOptions ? new Set() : null;
|
|
642
771
|
req.routeCount = 1;
|
|
@@ -673,9 +802,6 @@ function onNativeAborted() {
|
|
|
673
802
|
response.socket?.emit("error", err);
|
|
674
803
|
}
|
|
675
804
|
|
|
676
|
-
/**
|
|
677
|
-
*
|
|
678
|
-
*/
|
|
679
805
|
/**
|
|
680
806
|
* The per-request constants of a fully literal native registration. µWS matched the URL byte for
|
|
681
807
|
* byte against this exact pattern and dispatches by method, so the request constructor can take
|
|
@@ -683,15 +809,14 @@ function onNativeAborted() {
|
|
|
683
809
|
*
|
|
684
810
|
* @param {string} path the registered pattern, which is what getUrl() would have answered
|
|
685
811
|
* @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
812
|
*/
|
|
688
|
-
function nativePreset(path, method
|
|
813
|
+
function nativePreset(path, method) {
|
|
689
814
|
const endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
690
815
|
return {
|
|
691
816
|
path,
|
|
692
817
|
method,
|
|
693
818
|
endsWithSlash,
|
|
694
|
-
opPath:
|
|
819
|
+
opPath: path,
|
|
695
820
|
isOptions: method === "OPTIONS",
|
|
696
821
|
isHead: method === "HEAD",
|
|
697
822
|
// set at registration when the whole chain provably never reads a header, or never
|
|
@@ -752,6 +877,210 @@ const CALLBACK_ROUTER = 2;
|
|
|
752
877
|
* @param {any} req
|
|
753
878
|
* @param {any} app
|
|
754
879
|
*/
|
|
880
|
+
/**
|
|
881
|
+
* Reports a parameter that will not decode, unless something is already being reported.
|
|
882
|
+
*
|
|
883
|
+
* Matching a route decodes its parameters, and that happens while the walk is still looking for
|
|
884
|
+
* whoever should answer, including when it is looking for an error handler. Express does the same
|
|
885
|
+
* and keeps the first error it has: `layerError = layerError || match` in its router. Overwriting
|
|
886
|
+
* meant a middleware that had already refused the path, express.static answering Bad Request on an
|
|
887
|
+
* escape it could not decode, had its answer replaced by the decode failure of a route further down
|
|
888
|
+
* that was never going to run. Same status, different message, and only when a later route happens
|
|
889
|
+
* to match the same path. Found by fuzzing route tables against express.
|
|
890
|
+
*
|
|
891
|
+
* @param {any} req
|
|
892
|
+
* @param {any} route
|
|
893
|
+
* @param {any} err
|
|
894
|
+
*/
|
|
895
|
+
function raiseDecodeFailure(req, route, err) {
|
|
896
|
+
if (req._error) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
req._error = err;
|
|
900
|
+
req._errorKey = route.routeKey;
|
|
901
|
+
req._errorGroup = route.group;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// the verbs a body is read for unless the application says otherwise, which is the parsers' own
|
|
905
|
+
// list. A request with any other verb reaches a parser's method check and leaves through it
|
|
906
|
+
const BODY_METHODS = new Set(["POST", "PUT", "PATCH", "QUERY"]);
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Whether this layer can be stepped over for this request without changing a thing.
|
|
910
|
+
*
|
|
911
|
+
* Only the body parsers are ever asked. Their prologue leaves a request that said nothing about a
|
|
912
|
+
* body alone, whatever content type it carries, which is what `kGetSafe` already records for the
|
|
913
|
+
* header-skip analysis. Two conditions on top of that mark, and both are needed:
|
|
914
|
+
*
|
|
915
|
+
* The request must have said nothing about framing at all, a `content-length: 0` included. A parser
|
|
916
|
+
* that can see a length answers about the body it describes even when that body is empty: a zero
|
|
917
|
+
* length with a charset nobody can decode is a 415, in express and here.
|
|
918
|
+
*
|
|
919
|
+
* And the verb must be one no parser reads a body for. With no length and no transfer-encoding a
|
|
920
|
+
* POST still walks into the read, comes back with nothing, and leaves `req.body` as the empty value
|
|
921
|
+
* its parser produces, which is a thing a handler can see.
|
|
922
|
+
*
|
|
923
|
+
* What this is worth: a hop measured 367 microseconds per thousand requests on the machine this was
|
|
924
|
+
* written on, and the parser prologue it reaches measured 38. Ten to one, for a layer that had
|
|
925
|
+
* nothing to do.
|
|
926
|
+
*
|
|
927
|
+
* That number is also why fusing consecutive layers into one generated function keeps coming up,
|
|
928
|
+
* and why it is not here. Counted over a real front, morgan, helmet, compression, cors, the two body
|
|
929
|
+
* parsers, express-session, a middleware of one's own and express.static: three of the nine can be
|
|
930
|
+
* fused, and the longest run of fusable ones in a row is one. Fusing needs two. The rule was relaxed
|
|
931
|
+
* from "calls next once, unconditionally" to merely "calls next synchronously" and the answer did
|
|
932
|
+
* not move, because the six that fail all call next from inside a callback: they are asynchronous by
|
|
933
|
+
* nature, reading a body, stat-ing a file, loading a session. A layer that has not decided by the
|
|
934
|
+
* time it returns cannot be fused by any design that keeps the semantics. What fuses is a run of
|
|
935
|
+
* trivial middlewares, which is a benchmark shape rather than an application's.
|
|
936
|
+
*
|
|
937
|
+
* @param {any} route
|
|
938
|
+
* @param {any} req
|
|
939
|
+
* @returns {boolean}
|
|
940
|
+
*/
|
|
941
|
+
function stepsOver(route, req) {
|
|
942
|
+
if (route.bodyParserOnly !== true || req._hasBodyHeaders === true) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (BODY_METHODS.has(req.method)) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
// an application can add its own. Read once and kept, which is what the parser behind this
|
|
949
|
+
// layer does with the same setting: asking on every request measured 17 microseconds per
|
|
950
|
+
// thousand, a third of what stepping over the layer saves
|
|
951
|
+
if (route.bodyMethods === undefined) {
|
|
952
|
+
route.bodyMethods = req.app.get("body methods") ?? null;
|
|
953
|
+
}
|
|
954
|
+
return route.bodyMethods === null || !route.bodyMethods.includes(req.method);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Whether a route could answer a request for this path, judged on the pattern it was compiled to.
|
|
959
|
+
* A literal answers only itself; anything with a parameter or a wildcard answers what its regex
|
|
960
|
+
* says. Used where the question is "would this earlier route have had its turn first".
|
|
961
|
+
*
|
|
962
|
+
* @param {any} route
|
|
963
|
+
* @param {string} path
|
|
964
|
+
* @returns {boolean}
|
|
965
|
+
*/
|
|
966
|
+
function couldAnswer(route, path) {
|
|
967
|
+
if (route.pattern instanceof RegExp) {
|
|
968
|
+
return route.pattern.test(path);
|
|
969
|
+
}
|
|
970
|
+
return route.pattern === path;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Whether a layer written before a mount could answer a request for one of the paths inside it.
|
|
975
|
+
*
|
|
976
|
+
* A mount covers everything under its path, so this is a question about a subtree rather than about
|
|
977
|
+
* the mount point, and the two answers differ: `/a` and `/:p0/:p1/:p2` match none of each other's
|
|
978
|
+
* text, and both answer `/a/x/y`. µWS jumps straight to whichever leaf it registered, so a leaf a
|
|
979
|
+
* layer like this could have answered has to stay on the generic path, which is the only place
|
|
980
|
+
* express's registration order decides.
|
|
981
|
+
*
|
|
982
|
+
* Only layers with more segments than the mount path reach this: one with as few already matches
|
|
983
|
+
* the mount point itself, and _optimizeRoute has refused the mount before the walk gets here.
|
|
984
|
+
*
|
|
985
|
+
* Compared folded whichever way the routers are set. A wrong yes costs a leaf its native
|
|
986
|
+
* registration and nothing else.
|
|
987
|
+
*
|
|
988
|
+
* @param {{path: string, use: boolean, method: string, all: boolean}} guard
|
|
989
|
+
* @param {string} leafPath the leaf's absolute path, parameters and all
|
|
990
|
+
* @param {any} leaf
|
|
991
|
+
* @returns {boolean}
|
|
992
|
+
*/
|
|
993
|
+
function shadowsLeaf(guard, leafPath, leaf) {
|
|
994
|
+
if (!guard.all && guard.method !== leaf.method && !(guard.method === "HEAD" && leaf.method === "GET")) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
return pathsCanOverlap(guard.path.toLowerCase(), leafPath.toLowerCase(), guard.use);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The layers before a mount that answer some of what is inside it and not all of it, which is the
|
|
1002
|
+
* one thing neither the chain nor µWS's own choice can say: the chain runs what is in it without
|
|
1003
|
+
* matching again, and µWS picks by specificity. They are carried down the walk instead and asked
|
|
1004
|
+
* about every leaf, see shadowsLeaf.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {any} router the router the mount belongs to
|
|
1007
|
+
* @param {any} mount
|
|
1008
|
+
* @param {string} pathPrefix what the mounts above this one consumed
|
|
1009
|
+
* @param {any[]} chain the layers that always run before the mount, which need no guard
|
|
1010
|
+
* @param {any[]} inherited the guards from further out, since a mount two levels down is under
|
|
1011
|
+
* everything written before either of them
|
|
1012
|
+
* @returns {any[]|null} null when a path cannot be read segment by segment, which leaves the mount
|
|
1013
|
+
* to ordinary dispatch rather than guessing about it
|
|
1014
|
+
*/
|
|
1015
|
+
function guardsInside(router, mount, pathPrefix, chain, inherited) {
|
|
1016
|
+
let guards = inherited;
|
|
1017
|
+
for (const r of router._routes) {
|
|
1018
|
+
if (r.routeKey > mount.routeKey) {
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
if (r === mount || chain.includes(r)) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof r.path !== "string") {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
if (guards === inherited) {
|
|
1028
|
+
guards = [...inherited];
|
|
1029
|
+
}
|
|
1030
|
+
guards.push({ path: pathPrefix + r.path, use: r.use === true, method: r.method, all: r.all === true });
|
|
1031
|
+
}
|
|
1032
|
+
return guards;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Notes which application is current before a mounted one is entered, so that exact one comes back
|
|
1037
|
+
* when it hands over.
|
|
1038
|
+
*
|
|
1039
|
+
* Only an application takes it back. Express restores req.app by putting the request prototype
|
|
1040
|
+
* back, and it wraps a mounted application to do that only in Application#use: hang one off a plain
|
|
1041
|
+
* Router and nothing restores it, so whatever runs afterwards still reads the settings of the
|
|
1042
|
+
* application that was entered. Restoring regardless made a later res.send answer with the outer
|
|
1043
|
+
* application's etag setting where express answers with the inner.
|
|
1044
|
+
*
|
|
1045
|
+
* And what comes back is what was current, not the entered application's parent. Those differ the
|
|
1046
|
+
* moment a sub-app is entered from inside another sub-app that a plain Router mounted: the outer
|
|
1047
|
+
* one is still current, express puts that one back, and reaching for `.parent` skipped a level.
|
|
1048
|
+
* A 404 from the top application then carried an ETag under `app.set("etag", false)`, because the
|
|
1049
|
+
* settings answering were the inner application's. Found by fuzzing three levels of routers.
|
|
1050
|
+
*
|
|
1051
|
+
* The route is remembered alongside, so the pop can only ever take back what this same route put
|
|
1052
|
+
* there: a mounted application that answers instead of handing over leaves its entry behind, and
|
|
1053
|
+
* the request is over by then.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {any} walk
|
|
1056
|
+
* @param {any} route
|
|
1057
|
+
* @param {any} req
|
|
1058
|
+
*/
|
|
1059
|
+
function rememberApp(walk, route, req) {
|
|
1060
|
+
if (walk.router._isApplication && route.callbacks[0]?.constructor.name === "Application") {
|
|
1061
|
+
(req._appStack ??= []).push(route, req.app);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Puts back what rememberApp noted, if this is the route that noted it.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {any} route
|
|
1069
|
+
* @param {any} req
|
|
1070
|
+
*/
|
|
1071
|
+
function restoreApp(route, req) {
|
|
1072
|
+
const stack = req._appStack;
|
|
1073
|
+
if (stack !== undefined && stack.length > 0 && stack[stack.length - 2] === route) {
|
|
1074
|
+
const app = stack.pop();
|
|
1075
|
+
stack.pop();
|
|
1076
|
+
useApp(req, app);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @param {any} req
|
|
1082
|
+
* @param {any} app
|
|
1083
|
+
*/
|
|
755
1084
|
function useApp(req, app) {
|
|
756
1085
|
req.app = app;
|
|
757
1086
|
if (req.res) {
|
|
@@ -776,7 +1105,8 @@ function useApp(req, app) {
|
|
|
776
1105
|
const methods = ["all", ...METHODS.filter((method) => method !== "GET").map((method) => method.toLowerCase())];
|
|
777
1106
|
const supportedUwsMethods = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]);
|
|
778
1107
|
|
|
779
|
-
|
|
1108
|
+
// the same name rule patternToRegex reads, so a unicode name is found here too
|
|
1109
|
+
const regExParam = /:([$_\p{ID_Start}][$\u200c\u200d\p{ID_Continue}]*)/gu;
|
|
780
1110
|
|
|
781
1111
|
// Internals here are _underscore and not #private: a callable router is a function with the
|
|
782
1112
|
// router's properties copied onto it, and a # field cannot be copied, so #routes would throw
|
|
@@ -818,10 +1148,24 @@ function callablePrototypeFor(classPrototype) {
|
|
|
818
1148
|
* The default error page, which is the one Express produces: the stack in a pre, and nothing else.
|
|
819
1149
|
* What reaches it has already been redacted when the environment calls for it.
|
|
820
1150
|
*
|
|
1151
|
+
* The text is escaped, which is not decoration. An error message can carry anything a client sent,
|
|
1152
|
+
* a path or a header among them, and writing it into the page unescaped put whatever it held into
|
|
1153
|
+
* the markup. The Content-Security-Policy on this response stops a script there from running, but
|
|
1154
|
+
* a policy is a second line and not the first. finalhandler escapes and then puts the line breaks
|
|
1155
|
+
* and the indentation back as markup, and this reads the same as what it produces.
|
|
1156
|
+
*
|
|
821
1157
|
* @param {any} err
|
|
822
1158
|
* @returns {string}
|
|
823
1159
|
*/
|
|
824
1160
|
function generateErrorPageHtml(err) {
|
|
1161
|
+
const text = String(err?.stack ?? err)
|
|
1162
|
+
.replace(/&/g, "&")
|
|
1163
|
+
.replace(/</g, "<")
|
|
1164
|
+
.replace(/>/g, ">")
|
|
1165
|
+
.replace(/"/g, """)
|
|
1166
|
+
.replace(/'/g, "'")
|
|
1167
|
+
.replace(/\n/g, "<br>")
|
|
1168
|
+
.replace(/ {2}/g, " ");
|
|
825
1169
|
return (
|
|
826
1170
|
`<!DOCTYPE html>\n` +
|
|
827
1171
|
`<html lang="en">\n` +
|
|
@@ -830,7 +1174,7 @@ function generateErrorPageHtml(err) {
|
|
|
830
1174
|
`<title>Error</title>\n` +
|
|
831
1175
|
`</head>\n` +
|
|
832
1176
|
`<body>\n` +
|
|
833
|
-
`<pre>${
|
|
1177
|
+
`<pre>${text}</pre>\n` +
|
|
834
1178
|
`</body>\n` +
|
|
835
1179
|
`</html>\n`
|
|
836
1180
|
);
|
|
@@ -860,6 +1204,56 @@ module.exports = class Router extends EventEmitter {
|
|
|
860
1204
|
*/
|
|
861
1205
|
uwsApp;
|
|
862
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* Whether an unset routing flag reads on through the mount parent. Only an application does,
|
|
1209
|
+
* because express chains a mounted app's settings onto its parent's; a plain Router keeps
|
|
1210
|
+
* whatever its options said and nothing else.
|
|
1211
|
+
*
|
|
1212
|
+
* @type {boolean}
|
|
1213
|
+
*/
|
|
1214
|
+
_inheritsSettings = false;
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Whether this is an application rather than a plain router. Read on the hop out of a mount,
|
|
1218
|
+
* where only an application takes req.app back, and a field rather than a name comparison
|
|
1219
|
+
* because that sits on the dispatch path.
|
|
1220
|
+
*
|
|
1221
|
+
* @type {boolean}
|
|
1222
|
+
*/
|
|
1223
|
+
_isApplication = false;
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Whether anything served from here has been seen reading req.ip after the response, by which
|
|
1227
|
+
* point µWS has freed the address. Set once, from Request#parsedIp, and read on every request
|
|
1228
|
+
* after that. Here rather than on Application because a plain Router serves requests of its own
|
|
1229
|
+
* through the node shim.
|
|
1230
|
+
*
|
|
1231
|
+
* @type {boolean}
|
|
1232
|
+
*/
|
|
1233
|
+
needsIpAfterResponse = false;
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* How many requests still read the peer address up front whether or not anyone asks, so that
|
|
1237
|
+
* one of them can be the one that finds out. Counts to a hundred and stops: it used to be read
|
|
1238
|
+
* off a module-wide counter that wrapped at 100000, so the window reopened every time it did
|
|
1239
|
+
* and a hundred requests paid again for a discovery made long before.
|
|
1240
|
+
*
|
|
1241
|
+
* @type {number}
|
|
1242
|
+
*/
|
|
1243
|
+
_ipProbes = 0;
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* The two routing flags once read, undefined until then. Express passes caseSensitive and
|
|
1247
|
+
* strict in when it builds a router and never looks at them again, so they are frozen here at
|
|
1248
|
+
* the first read rather than resolved per request.
|
|
1249
|
+
*
|
|
1250
|
+
* @type {boolean|undefined}
|
|
1251
|
+
*/
|
|
1252
|
+
_strictFlag;
|
|
1253
|
+
|
|
1254
|
+
/** @type {boolean|undefined} */
|
|
1255
|
+
_caseFlag;
|
|
1256
|
+
|
|
863
1257
|
/**
|
|
864
1258
|
* @param {object} [settings] router options. caseSensitive and strict are accepted under the
|
|
865
1259
|
* names Express's Router takes, and stored under the setting names the rest of the code reads
|
|
@@ -992,6 +1386,53 @@ module.exports = class Router extends EventEmitter {
|
|
|
992
1386
|
return this.createRoute("GET", path, this, ...callbacks);
|
|
993
1387
|
}
|
|
994
1388
|
|
|
1389
|
+
/**
|
|
1390
|
+
* A routing flag, read once and kept. Express builds a router's matcher the first time the
|
|
1391
|
+
* router is needed and hands it caseSensitive and strict there, so a mount that happens after
|
|
1392
|
+
* that, or an app.set() that happens after that, cannot change how this router matches. Asking
|
|
1393
|
+
* per request instead would let a strict application make every router mounted on it strict,
|
|
1394
|
+
* which express does not do.
|
|
1395
|
+
*
|
|
1396
|
+
* @param {string} key the setting name
|
|
1397
|
+
* @returns {boolean}
|
|
1398
|
+
*/
|
|
1399
|
+
_routingFlag(key) {
|
|
1400
|
+
const own = this.settings[key];
|
|
1401
|
+
if (typeof own !== "undefined") {
|
|
1402
|
+
return Boolean(own);
|
|
1403
|
+
}
|
|
1404
|
+
return this._inheritsSettings && this.parent ? Boolean(this.parent.get(key)) : false;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Reads both flags at once, the first time either is wanted, because express reads both at
|
|
1409
|
+
* once too: it passes them together to the router it builds. Freezing them apart would let a
|
|
1410
|
+
* router end up strict from the moment before a mount and case sensitive from the moment
|
|
1411
|
+
* after it, which is a state express can never be in.
|
|
1412
|
+
*/
|
|
1413
|
+
_freezeRoutingFlags() {
|
|
1414
|
+
if (this._strictFlag === undefined) {
|
|
1415
|
+
this._strictFlag = this._routingFlag("strict routing");
|
|
1416
|
+
this._caseFlag = this._routingFlag("case sensitive routing");
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* @returns {boolean} whether this router tells /things from /things/
|
|
1422
|
+
*/
|
|
1423
|
+
_strictRouting() {
|
|
1424
|
+
this._freezeRoutingFlags();
|
|
1425
|
+
return /** @type {boolean} */ (this._strictFlag);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* @returns {boolean} whether this router tells /Things from /things
|
|
1430
|
+
*/
|
|
1431
|
+
_caseSensitive() {
|
|
1432
|
+
this._freezeRoutingFlags();
|
|
1433
|
+
return /** @type {boolean} */ (this._caseFlag);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
995
1436
|
/**
|
|
996
1437
|
* The pattern matching everything the mounts on this request have consumed so far, which is
|
|
997
1438
|
* what a nested router strips off the path before matching against it. Cached per stack, since
|
|
@@ -1022,7 +1463,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1022
1463
|
const stackPattern = fullStack.includes(":")
|
|
1023
1464
|
? fullStack.replace(/(\\?):(\w+)/g, (whole, escaped, name, at) => (escaped ? whole : ":m" + at))
|
|
1024
1465
|
: fullStack;
|
|
1025
|
-
|
|
1466
|
+
// insensitive whatever this router says, because this only finds again a prefix that
|
|
1467
|
+
// has already been accepted, by the routers that own those mounts and under their
|
|
1468
|
+
// rules. A case sensitive router mounted on an insensitive app is reached as /LIST
|
|
1469
|
+
// while it is registered as /list, and compiling this one its way left the prefix in
|
|
1470
|
+
// place and every parameter below it unread
|
|
1471
|
+
fullMountpath = patternToRegex(stackPattern, true, false);
|
|
1026
1472
|
this._mountpathCache.set(fullStack, fullMountpath);
|
|
1027
1473
|
}
|
|
1028
1474
|
return fullMountpath;
|
|
@@ -1038,18 +1484,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1038
1484
|
* @returns {boolean}
|
|
1039
1485
|
*/
|
|
1040
1486
|
_pathMatches(route, req) {
|
|
1487
|
+
// the path as it arrived, mount prefixes aside: whether a trailing slash is allowed is
|
|
1488
|
+
// written into the pattern, where express writes it too
|
|
1041
1489
|
let path = req._opPath;
|
|
1042
1490
|
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
1491
|
// the line above turns the root path into the empty string, which no pattern is written
|
|
1054
1492
|
// against. A regex route was tested against it and app.get("*path") answered every request
|
|
1055
1493
|
// but "/"
|
|
@@ -1061,11 +1499,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
1061
1499
|
if (pattern === "/*") {
|
|
1062
1500
|
return true;
|
|
1063
1501
|
}
|
|
1064
|
-
if (!this.
|
|
1502
|
+
if (!this._caseSensitive()) {
|
|
1065
1503
|
path = path.toLowerCase();
|
|
1066
1504
|
pattern = pattern.toLowerCase();
|
|
1067
1505
|
}
|
|
1068
|
-
|
|
1506
|
+
if (pattern === path) {
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
// a literal path is compared as text rather than compiled, so the trailing slash a
|
|
1510
|
+
// pattern would have carried as "/?" is allowed here instead. The registered path has
|
|
1511
|
+
// had its own taken off already, unless it is the root
|
|
1512
|
+
return (
|
|
1513
|
+
!this._strictRouting() &&
|
|
1514
|
+
path.length === pattern.length + 1 &&
|
|
1515
|
+
path.charCodeAt(path.length - 1) === 0x2f &&
|
|
1516
|
+
path.startsWith(pattern)
|
|
1517
|
+
);
|
|
1069
1518
|
}
|
|
1070
1519
|
if (pattern === EMPTY_REGEX) {
|
|
1071
1520
|
return true;
|
|
@@ -1105,12 +1554,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1105
1554
|
// a mount always drops it, strict routing or not: strictness is about the end of a
|
|
1106
1555
|
// path, and a mount has none. Express registers its use layers with strict off
|
|
1107
1556
|
if (
|
|
1108
|
-
(method === "USE" || !this.
|
|
1557
|
+
(method === "USE" || !this._strictRouting()) &&
|
|
1109
1558
|
typeof path === "string" &&
|
|
1110
1559
|
path.endsWith("/") &&
|
|
1111
1560
|
path !== "/"
|
|
1112
1561
|
) {
|
|
1113
|
-
|
|
1562
|
+
// every one of them, not the last: express loosens with /\/+$/, so a route written
|
|
1563
|
+
// "/test//" is registered as "/test" and answers "/test" and "/test/" but not the
|
|
1564
|
+
// path it was written as
|
|
1565
|
+
path = path.replace(/\/+$/, "");
|
|
1114
1566
|
}
|
|
1115
1567
|
if (path === "*") {
|
|
1116
1568
|
path = "/{*splat}";
|
|
@@ -1120,9 +1572,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1120
1572
|
path,
|
|
1121
1573
|
pattern:
|
|
1122
1574
|
method === "USE" || needsConversionToRegex(path)
|
|
1123
|
-
?
|
|
1124
|
-
// default parameter would silently turn back into case-sensitive
|
|
1125
|
-
patternToRegex(path, method === "USE", Boolean(this.get("case sensitive routing")))
|
|
1575
|
+
? patternToRegex(path, method === "USE", this._caseSensitive(), this._strictRouting())
|
|
1126
1576
|
: path,
|
|
1127
1577
|
callbacks,
|
|
1128
1578
|
// instanceof walks a prototype chain and length is a property load, and both used
|
|
@@ -1134,12 +1584,23 @@ module.exports = class Router extends EventEmitter {
|
|
|
1134
1584
|
? CALLBACK_ERROR
|
|
1135
1585
|
: CALLBACK_PLAIN
|
|
1136
1586
|
),
|
|
1587
|
+
// A body parser, and nothing else: they carry the mark that says their prologue
|
|
1588
|
+
// leaves a request that declared no body alone. Reaching one costs a hop, and the
|
|
1589
|
+
// hop measures ten times what the prologue does, so a request that provably gets
|
|
1590
|
+
// nothing out of it steps over the whole layer. See stepsOver
|
|
1591
|
+
bodyParserOnly: method === "USE" && callbacks.length === 1 && callbacks[0][kGetSafe] === true,
|
|
1592
|
+
// the "body methods" setting as it stood the first time this layer was reached,
|
|
1593
|
+
// kept the way the parser behind it keeps it. undefined until then
|
|
1594
|
+
bodyMethods: undefined,
|
|
1137
1595
|
// a mount written as a RegExp matches a piece of path that is not known until a
|
|
1138
1596
|
// request comes in, so its stack entry cannot be the path itself
|
|
1139
1597
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
1140
1598
|
// written by the application, so express matches it as it stands
|
|
1141
1599
|
userRegexp: path instanceof RegExp,
|
|
1142
1600
|
routeKey: routeKey++,
|
|
1601
|
+
// which app.route() this came from, when it came from one, so the routes it built
|
|
1602
|
+
// count as one route where an error is concerned. undefined for every other route
|
|
1603
|
+
group: this._pendingGroup,
|
|
1143
1604
|
// the router this was registered on. Ordinary dispatch is done by that router, so
|
|
1144
1605
|
// it could ask itself, but an optimized chain is walked by the app whatever it
|
|
1145
1606
|
// contains, and param() callbacks belong to the router that declared them
|
|
@@ -1197,8 +1658,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1197
1658
|
// so the text comparisons below run on the folded form. µWS itself still matches bytes:
|
|
1198
1659
|
// a request in the registered case takes the chain, any other case takes the fallback,
|
|
1199
1660
|
// and both answer as express would as long as the chain agrees with registration order
|
|
1200
|
-
const caseSensitive =
|
|
1661
|
+
const caseSensitive = this._caseSensitive();
|
|
1201
1662
|
const routePathFolded = caseSensitive || typeof route.path !== "string" ? route.path : route.path.toLowerCase();
|
|
1663
|
+
// whether this route answers only the path as written, or the one with a trailing slash too
|
|
1664
|
+
const strictHere = (route.owner ?? this)._strictRouting();
|
|
1665
|
+
/** @type {string[]|null} earlier literals a case variant could smuggle a request past */
|
|
1666
|
+
let caseGuards = null;
|
|
1202
1667
|
|
|
1203
1668
|
for (let i = 0; i < routes.length; i++) {
|
|
1204
1669
|
const r = routes[i];
|
|
@@ -1212,6 +1677,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
1212
1677
|
if (!r.all && r.method !== route.method) {
|
|
1213
1678
|
// check if the methods are compatible (GET and HEAD)
|
|
1214
1679
|
if (!(r.method === "HEAD" && route.method === "GET")) {
|
|
1680
|
+
// A mount is registered ALL, because what lives under it can answer any
|
|
1681
|
+
// method, and this chain is computed once for all of them. So an earlier
|
|
1682
|
+
// route of some other method is not irrelevant here the way it is for a
|
|
1683
|
+
// plain route: it belongs in the chain of the leaves that share its method
|
|
1684
|
+
// and in no other, and one chain cannot say that. µWS would then jump
|
|
1685
|
+
// straight to a leaf and answer as though the earlier route did not exist,
|
|
1686
|
+
// which is what let a literal route inside a mounted router beat a parameter
|
|
1687
|
+
// route written before the mount. Leave the mount to ordinary dispatch,
|
|
1688
|
+
// where express's own order is what decides.
|
|
1689
|
+
if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1215
1692
|
continue;
|
|
1216
1693
|
}
|
|
1217
1694
|
}
|
|
@@ -1241,9 +1718,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
1241
1718
|
}
|
|
1242
1719
|
|
|
1243
1720
|
// 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
|
|
1721
|
+
// its literal ":name" text would let an earlier regex in on requests it never matches.
|
|
1722
|
+
// Both spellings of this route's path are tried, because without strict routing it
|
|
1723
|
+
// answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
|
|
1724
|
+
// still an earlier pattern that answers a request this registration would take.
|
|
1245
1725
|
if (
|
|
1246
|
-
(r.pattern instanceof RegExp &&
|
|
1726
|
+
(r.pattern instanceof RegExp &&
|
|
1727
|
+
(!withParams || r.use) &&
|
|
1728
|
+
(r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
|
|
1247
1729
|
(typeof r.pattern === "string" &&
|
|
1248
1730
|
(r.pattern === route.path ||
|
|
1249
1731
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
@@ -1281,18 +1763,26 @@ module.exports = class Router extends EventEmitter {
|
|
|
1281
1763
|
continue;
|
|
1282
1764
|
}
|
|
1283
1765
|
// 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
|
|
1285
|
-
// about bytes, so under insensitive routing it only holds when no literal hides
|
|
1286
|
-
// behind a case difference
|
|
1766
|
+
// more specific registration, so this chain never sees those paths
|
|
1287
1767
|
if (
|
|
1288
1768
|
!r.optimizedPath ||
|
|
1289
1769
|
!uwsPrefersEarlier(r.path, route.path) ||
|
|
1290
|
-
(!caseSensitive &&
|
|
1770
|
+
(!caseSensitive && route.path !== routePathFolded)
|
|
1291
1771
|
) {
|
|
1292
1772
|
return false;
|
|
1293
1773
|
}
|
|
1774
|
+
// that argument is about bytes. Under insensitive routing "/POSTS" byte-matches no
|
|
1775
|
+
// registration of "/posts", so µWS hands it here instead, where this chain would
|
|
1776
|
+
// answer as if the earlier route did not exist. The literal is remembered so the
|
|
1777
|
+
// registration can send those requests to the generic router, which is the only place
|
|
1778
|
+
// express's own order can decide; a path with no letter in it has no other case to
|
|
1779
|
+
// arrive in and needs no guard
|
|
1780
|
+
if (!caseSensitive && HAS_LETTER.test(r.path)) {
|
|
1781
|
+
(caseGuards ??= []).push(r.path);
|
|
1782
|
+
}
|
|
1294
1783
|
}
|
|
1295
1784
|
optimizedPath.push(route);
|
|
1785
|
+
route._caseGuards = caseGuards;
|
|
1296
1786
|
|
|
1297
1787
|
return optimizedPath;
|
|
1298
1788
|
}
|
|
@@ -1307,8 +1797,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1307
1797
|
return;
|
|
1308
1798
|
}
|
|
1309
1799
|
|
|
1310
|
-
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts
|
|
1311
|
-
|
|
1800
|
+
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
1801
|
+
// carries what was written before them and answers only part of what is under them
|
|
1802
|
+
const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
|
|
1312
1803
|
for (const route of router._routes) {
|
|
1313
1804
|
if (route.use) {
|
|
1314
1805
|
// only sole-callback mounts. Case rules do not gate the walk: each level's
|
|
@@ -1324,24 +1815,43 @@ module.exports = class Router extends EventEmitter {
|
|
|
1324
1815
|
) {
|
|
1325
1816
|
let pathToMount = router._optimizeRoute(route, router._routes);
|
|
1326
1817
|
if (!pathToMount) {
|
|
1818
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
1327
1819
|
continue;
|
|
1328
1820
|
}
|
|
1329
1821
|
pathToMount = pathToMount.slice(0, -1);
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1822
|
+
const guards = guardsInside(router, route, pathPrefix, pathToMount, outerGuards);
|
|
1823
|
+
if (guards === null) {
|
|
1824
|
+
route._whyGeneric = "a path written before it cannot be read segment by segment";
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
route._walkedInto = true;
|
|
1828
|
+
walk(
|
|
1829
|
+
route.callbacks[0],
|
|
1830
|
+
pathPrefix + route.path,
|
|
1831
|
+
[
|
|
1832
|
+
...chainPrefix,
|
|
1833
|
+
...pathToMount,
|
|
1834
|
+
{
|
|
1835
|
+
...route,
|
|
1836
|
+
callbacks: [],
|
|
1837
|
+
callbackKinds: [],
|
|
1838
|
+
keepMount: true,
|
|
1839
|
+
// mounted sub-apps become req.app during their dispatch, like express
|
|
1840
|
+
mountApp:
|
|
1841
|
+
route.callbacks[0].constructor.name === "Application"
|
|
1842
|
+
? route.callbacks[0]
|
|
1843
|
+
: undefined
|
|
1844
|
+
}
|
|
1845
|
+
],
|
|
1846
|
+
guards
|
|
1847
|
+
);
|
|
1848
|
+
} else {
|
|
1849
|
+
// said once here rather than at each condition above: a mount is walked into
|
|
1850
|
+
// only when µWS can match its path on its own and it carries exactly one
|
|
1851
|
+
// router, and those are the two things worth telling anyone about
|
|
1852
|
+
route._whyGeneric = !(route.callbacks.length === 1 && route.callbacks[0] instanceof Router)
|
|
1853
|
+
? "it is middleware rather than a single mounted router"
|
|
1854
|
+
: "µWS cannot match this mount path on its own";
|
|
1345
1855
|
}
|
|
1346
1856
|
// µWS picks by specificity and Express by registration order, so the chain
|
|
1347
1857
|
// computed for whichever route µWS lands on runs everything that could have
|
|
@@ -1354,8 +1864,19 @@ module.exports = class Router extends EventEmitter {
|
|
|
1354
1864
|
(!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)))) &&
|
|
1355
1865
|
supportedUwsMethods.has(route.method)
|
|
1356
1866
|
) {
|
|
1867
|
+
// something outside this router, written before the mount it is in, that could
|
|
1868
|
+
// answer this exact path. µWS would jump here and never give it its turn
|
|
1869
|
+
if (outerGuards.length > 0 && typeof route.path === "string") {
|
|
1870
|
+
const absolute = pathPrefix + route.path;
|
|
1871
|
+
const guard = outerGuards.find((g) => shadowsLeaf(g, absolute, route));
|
|
1872
|
+
if (guard) {
|
|
1873
|
+
route._whyGeneric = `${guard.path} is written before the mount it is in and answers the same paths`;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1357
1877
|
const leafPath = router._optimizeRoute(route, router._routes);
|
|
1358
1878
|
if (!leafPath) {
|
|
1879
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
1359
1880
|
continue;
|
|
1360
1881
|
}
|
|
1361
1882
|
// param route earlier in the same router would steal this static path
|
|
@@ -1368,6 +1889,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1368
1889
|
shadow.path !== route.path &&
|
|
1369
1890
|
shadow.pattern instanceof RegExp
|
|
1370
1891
|
) {
|
|
1892
|
+
route._whyGeneric = `the parameter route ${shadow.path} is written before it`;
|
|
1371
1893
|
continue;
|
|
1372
1894
|
}
|
|
1373
1895
|
}
|
|
@@ -1382,6 +1904,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
1382
1904
|
pattern: pathPrefix + route.path,
|
|
1383
1905
|
optimizedRouter: true
|
|
1384
1906
|
};
|
|
1907
|
+
if (route._caseGuards) {
|
|
1908
|
+
// compared against the whole path µWS matched, so they carry the mount
|
|
1909
|
+
// prefix, folded along with the rest of it
|
|
1910
|
+
registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
|
|
1911
|
+
}
|
|
1385
1912
|
this._registerUwsRoute(registered, chain);
|
|
1386
1913
|
// the chain holds the original object, so the request-time guard has to
|
|
1387
1914
|
// find the computed fields there, or a mounted param route extracts its
|
|
@@ -1389,14 +1916,24 @@ module.exports = class Router extends EventEmitter {
|
|
|
1389
1916
|
// adds no parameter of its own
|
|
1390
1917
|
route.optimizedParams = registered.optimizedParams;
|
|
1391
1918
|
route.optimizedPath = registered.optimizedPath;
|
|
1919
|
+
// and what was decided about it, for the same reason: the copy is thrown
|
|
1920
|
+
// away and the profile reads the route the application actually holds
|
|
1921
|
+
route._native = registered._native;
|
|
1392
1922
|
} else {
|
|
1393
1923
|
this._registerUwsRoute(route, chain);
|
|
1394
1924
|
}
|
|
1925
|
+
} else if (!supportedUwsMethods.has(route.method)) {
|
|
1926
|
+
route._whyGeneric = `µWS does not serve ${route.method}`;
|
|
1927
|
+
} else if (canBeOptimizedWithParams(route.path)) {
|
|
1928
|
+
// eligible but for the overlap test, which only applies inside a mount
|
|
1929
|
+
route._whyGeneric = "a route after it in the same mounted router could answer the same paths";
|
|
1930
|
+
} else {
|
|
1931
|
+
route._whyGeneric = "µWS cannot match this path on its own";
|
|
1395
1932
|
}
|
|
1396
1933
|
}
|
|
1397
1934
|
};
|
|
1398
1935
|
|
|
1399
|
-
walk(this, "", []);
|
|
1936
|
+
walk(this, "", [], []);
|
|
1400
1937
|
}
|
|
1401
1938
|
|
|
1402
1939
|
/**
|
|
@@ -1445,7 +1982,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1445
1982
|
*/
|
|
1446
1983
|
_isFollowedByAnOverlap(route, routes) {
|
|
1447
1984
|
// folded under insensitive routing, where a case variant answers the same requests
|
|
1448
|
-
const caseSensitive =
|
|
1985
|
+
const caseSensitive = this._caseSensitive();
|
|
1449
1986
|
const routePath = caseSensitive ? route.path : route.path.toLowerCase();
|
|
1450
1987
|
for (let i = routes.length - 1; i >= 0; i--) {
|
|
1451
1988
|
const later = routes[i];
|
|
@@ -1487,6 +2024,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
1487
2024
|
if (route.path.includes(":")) {
|
|
1488
2025
|
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
1489
2026
|
}
|
|
2027
|
+
// null for almost every route: only a parameter route with an earlier literal that a case
|
|
2028
|
+
// variant could slip past carries one, see _optimizeRoute. Built once here, and matched
|
|
2029
|
+
// insensitively, since that is the folding the guard exists for
|
|
2030
|
+
const caseGuards = route._caseGuards
|
|
2031
|
+
? route._caseGuards.map((p) =>
|
|
2032
|
+
needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
|
|
2033
|
+
)
|
|
2034
|
+
: null;
|
|
1490
2035
|
const makeHandler = (chain, preset, skips) => {
|
|
1491
2036
|
// the mutable object a granted skip lives on, so a middleware arriving after
|
|
1492
2037
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
@@ -1508,6 +2053,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1508
2053
|
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
1509
2054
|
// microtask, which is where the await used to resume, so the visible order holds
|
|
1510
2055
|
return (res, req) => {
|
|
2056
|
+
// a request that is an earlier literal in another case: express answers it with
|
|
2057
|
+
// that route, and the chain here does not contain it, so the generic router takes
|
|
2058
|
+
// this one
|
|
2059
|
+
if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
|
|
2060
|
+
// an application is what registers native routes, and only it serves
|
|
2061
|
+
return /** @type {any} */ (this)._serveGeneric(res, req);
|
|
2062
|
+
}
|
|
1511
2063
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
1512
2064
|
const response = request.res;
|
|
1513
2065
|
if (optimizedParams) {
|
|
@@ -1548,7 +2100,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1548
2100
|
// the route's own router decides, not the app running the registration: a router created
|
|
1549
2101
|
// with { strict: true } and mounted on an app without it does not answer /things/, and
|
|
1550
2102
|
// registering that path here is the only way it could
|
|
1551
|
-
const strictHere =
|
|
2103
|
+
const strictHere = (route.owner ?? this)._strictRouting();
|
|
1552
2104
|
|
|
1553
2105
|
// Whether requests served by this registration may skip the header copy: GET and its
|
|
1554
2106
|
// HEAD twins only, the app must not compute etags (send would consult freshness
|
|
@@ -1574,7 +2126,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1574
2126
|
}
|
|
1575
2127
|
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
1576
2128
|
const makePreset = (path, method, skips) => {
|
|
1577
|
-
const preset = nativePreset(path, method
|
|
2129
|
+
const preset = nativePreset(path, method);
|
|
1578
2130
|
if (skips.skipHeaders || skips.skipQuery) {
|
|
1579
2131
|
preset.skipHeaders = skips.skipHeaders;
|
|
1580
2132
|
preset.skipQuery = skips.skipQuery;
|
|
@@ -1601,6 +2153,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1601
2153
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
1602
2154
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
1603
2155
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
2156
|
+
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
2157
|
+
// guard could not: a route that needs one has to stay an ordinary handler
|
|
2158
|
+
caseGuards === null &&
|
|
1604
2159
|
!resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
|
|
1605
2160
|
this.get("declarative responses") // must have declarative responses enabled
|
|
1606
2161
|
) {
|
|
@@ -1612,6 +2167,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
1612
2167
|
replacedPath = route.path.replace(regExParam, ":x");
|
|
1613
2168
|
}
|
|
1614
2169
|
|
|
2170
|
+
// what listen() settled about this route, kept so `npx fulmine profile` can print it rather
|
|
2171
|
+
// than making anyone read the source or instrument it. Written once, during compilation,
|
|
2172
|
+
// so no request pays for it
|
|
2173
|
+
route._native = {
|
|
2174
|
+
path: replacedPath,
|
|
2175
|
+
declarative: fn !== jsFn,
|
|
2176
|
+
skipHeaders: getSkips.skipHeaders === true,
|
|
2177
|
+
skipQuery: getSkips.skipQuery === true,
|
|
2178
|
+
ahead: optimizedPath.length - 1,
|
|
2179
|
+
guards: caseGuards ? caseGuards.length : 0
|
|
2180
|
+
};
|
|
2181
|
+
|
|
1615
2182
|
this.uwsApp[method](replacedPath, fn);
|
|
1616
2183
|
if (!strictHere && route.path[route.path.length - 1] !== "/") {
|
|
1617
2184
|
// a declarative response answers the twin as itself; a preset handler cannot be
|
|
@@ -1739,10 +2306,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1739
2306
|
// asking for each name in turn rather than walking the groups object, which is a
|
|
1740
2307
|
// null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
|
|
1741
2308
|
// worked out when the pattern was compiled instead of searching an array for it
|
|
1742
|
-
const { paramNames, isWildcard } = meta;
|
|
2309
|
+
const { paramNames, outputNames, isWildcard } = meta;
|
|
1743
2310
|
for (let i = 0, len = paramNames.length; i < len; i++) {
|
|
1744
|
-
const name =
|
|
1745
|
-
const value = groups[
|
|
2311
|
+
const name = outputNames[i];
|
|
2312
|
+
const value = groups[paramNames[i]];
|
|
1746
2313
|
// an optional group that did not match is absent in v5, not present as undefined
|
|
1747
2314
|
if (value === undefined) {
|
|
1748
2315
|
continue;
|
|
@@ -1776,36 +2343,29 @@ module.exports = class Router extends EventEmitter {
|
|
|
1776
2343
|
req.params[name] = decodeParam(req.optimizedParams[name]);
|
|
1777
2344
|
}
|
|
1778
2345
|
} catch (err) {
|
|
1779
|
-
req
|
|
1780
|
-
req._errorKey = route.routeKey;
|
|
2346
|
+
raiseDecodeFailure(req, route, err);
|
|
1781
2347
|
return "route";
|
|
1782
2348
|
}
|
|
1783
2349
|
} else if (route.complex) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const fullMountpath = this.getFullMountpath(req);
|
|
1787
|
-
if (fullMountpath !== EMPTY_REGEX) {
|
|
1788
|
-
path = path.replace(fullMountpath, "");
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
2350
|
+
// the path with the mounts taken off, which is what _opPath is
|
|
2351
|
+
const path = req._opPath;
|
|
1791
2352
|
try {
|
|
1792
2353
|
req.params = this._extractParams(route.pattern, path);
|
|
1793
2354
|
} catch (err) {
|
|
1794
2355
|
// a parameter that will not decode. Express throws out of the match and lets the
|
|
1795
2356
|
// error reach the error handler, which answers 400, so the route is skipped rather
|
|
1796
2357
|
// than run with a value nobody can read.
|
|
1797
|
-
req
|
|
1798
|
-
req._errorKey = route.routeKey;
|
|
2358
|
+
raiseDecodeFailure(req, route, err);
|
|
1799
2359
|
return "route";
|
|
1800
2360
|
}
|
|
1801
|
-
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
2361
|
+
if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
|
|
1802
2362
|
req.params = mergeParams(req.params, req._paramStack);
|
|
1803
2363
|
}
|
|
1804
2364
|
} else {
|
|
1805
2365
|
// express 5 gives every matched route null-prototype params; only a pathless
|
|
1806
2366
|
// middleware layer keeps the plain object, as its router hands one to fast_slash
|
|
1807
2367
|
req.params = route.use && route.path === "" ? {} : Object.create(null);
|
|
1808
|
-
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
2368
|
+
if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
|
|
1809
2369
|
req.params = mergeParams(req.params, req._paramStack);
|
|
1810
2370
|
}
|
|
1811
2371
|
}
|
|
@@ -1819,6 +2379,27 @@ module.exports = class Router extends EventEmitter {
|
|
|
1819
2379
|
return true;
|
|
1820
2380
|
}
|
|
1821
2381
|
|
|
2382
|
+
/**
|
|
2383
|
+
* Whether this route's parameters carry a percent escape that will not decode. Asked only of a
|
|
2384
|
+
* route whose path matched and whose method did not, which express still decodes: the 400 it
|
|
2385
|
+
* answers there is what this reproduces.
|
|
2386
|
+
*
|
|
2387
|
+
* @param {any} route
|
|
2388
|
+
* @param {any} req
|
|
2389
|
+
* @returns {boolean}
|
|
2390
|
+
*/
|
|
2391
|
+
_paramsFailToDecode(route, req) {
|
|
2392
|
+
if (!route.complex) {
|
|
2393
|
+
return false;
|
|
2394
|
+
}
|
|
2395
|
+
try {
|
|
2396
|
+
this._extractParams(route.pattern, req._opPath);
|
|
2397
|
+
return false;
|
|
2398
|
+
} catch {
|
|
2399
|
+
return true;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
1822
2403
|
/**
|
|
1823
2404
|
* Runs the app.param() callbacks for the parameters this route matched, and says whether the
|
|
1824
2405
|
* route may run.
|
|
@@ -1864,6 +2445,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1864
2445
|
if (err !== "route") {
|
|
1865
2446
|
req._error = err;
|
|
1866
2447
|
req._errorKey = route.routeKey;
|
|
2448
|
+
req._errorGroup = route.group;
|
|
1867
2449
|
}
|
|
1868
2450
|
// the route is skipped either way: an error carries on to the error handlers
|
|
1869
2451
|
return resolve("route");
|
|
@@ -2037,15 +2619,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
2037
2619
|
* @returns {object} an object with one method per HTTP verb, each returning it again
|
|
2038
2620
|
*/
|
|
2039
2621
|
route(path) {
|
|
2622
|
+
// everything hung off one app.route() shares this, which is what makes them one route as
|
|
2623
|
+
// far as an error is concerned, see errorHop
|
|
2624
|
+
const group = ++routeGroups;
|
|
2040
2625
|
const fns = new NullObject();
|
|
2041
|
-
|
|
2042
|
-
|
|
2626
|
+
const inGroup = (method, callbacks) => {
|
|
2627
|
+
this._pendingGroup = group;
|
|
2628
|
+
try {
|
|
2043
2629
|
return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
return this.createRoute("GET", path, /** @type {any} */ (fns), ...callbacks);
|
|
2630
|
+
} finally {
|
|
2631
|
+
this._pendingGroup = undefined;
|
|
2632
|
+
}
|
|
2048
2633
|
};
|
|
2634
|
+
for (const method of methods) {
|
|
2635
|
+
fns[method] = (...callbacks) => inGroup(method, callbacks);
|
|
2636
|
+
}
|
|
2637
|
+
fns.get = (...callbacks) => inGroup("GET", callbacks);
|
|
2049
2638
|
return fns;
|
|
2050
2639
|
}
|
|
2051
2640
|
|
|
@@ -2074,14 +2663,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
2074
2663
|
*
|
|
2075
2664
|
* @param {any} request
|
|
2076
2665
|
* @param {any} response
|
|
2666
|
+
* @param {Set<string>} methods the verbs the answering router knows, which are its own
|
|
2077
2667
|
*/
|
|
2078
|
-
_sendOptionsReply(request, response) {
|
|
2668
|
+
_sendOptionsReply(request, response, methods) {
|
|
2079
2669
|
if (response._headWritten) {
|
|
2080
2670
|
throw new Error("Cannot set headers after they are sent to the client");
|
|
2081
2671
|
}
|
|
2082
2672
|
// Express 5 sorts the methods and joins them with ", ", so the header reads the same
|
|
2083
2673
|
// regardless of the order the routes happened to be registered in
|
|
2084
|
-
const allowedMethods = Array.from(
|
|
2674
|
+
const allowedMethods = Array.from(methods).sort().join(", ");
|
|
2085
2675
|
response.setHeader("Allow", allowedMethods);
|
|
2086
2676
|
// the router package answers this one itself, with a plain-text body, the nosniff
|
|
2087
2677
|
// header and end() rather than send(), so no ETag comes with it
|
|
@@ -2104,7 +2694,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
2104
2694
|
}
|
|
2105
2695
|
if (request._isOptions && request._matchedMethods.size > 0) {
|
|
2106
2696
|
try {
|
|
2107
|
-
this._sendOptionsReply(request, response);
|
|
2697
|
+
this._sendOptionsReply(request, response, request._matchedMethods);
|
|
2108
2698
|
} catch (err) {
|
|
2109
2699
|
// a head already written: the error answers instead, as express's does
|
|
2110
2700
|
this._handleError(err, null, request, response);
|