fulmine.js 5.1.9 → 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/NOTICE +29 -2
- package/README.md +148 -62
- package/package.json +6 -2
- package/src/application.js +97 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +97 -25
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +110 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +357 -38
- package/src/response.js +79 -15
- package/src/router.js +686 -160
- package/src/types.d.ts +52 -3
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +239 -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,7 +37,69 @@ 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");
|
|
41
|
+
const { checkBehavior } = require("./websocket.js");
|
|
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
|
+
}
|
|
40
103
|
|
|
41
104
|
// every method the declarative compiler can emit: a patched one must disable compilation, or the
|
|
42
105
|
// patch would be honoured everywhere but on compiled routes
|
|
@@ -88,6 +151,29 @@ class Walk {
|
|
|
88
151
|
// bound, not wrapped in an arrow: an arrow forwarding into step() is one more call on every
|
|
89
152
|
// hop, and it measured 495 microseconds per thousand requests of nothing else
|
|
90
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");
|
|
91
177
|
}
|
|
92
178
|
|
|
93
179
|
/**
|
|
@@ -107,15 +193,40 @@ class Walk {
|
|
|
107
193
|
return;
|
|
108
194
|
}
|
|
109
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
|
+
}
|
|
110
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;
|
|
111
212
|
// written out rather than through a predicate handed to findIndexStartingFrom, which
|
|
112
213
|
// was one closure per hop of every request not on a compiled chain
|
|
113
214
|
for (; routeIndex < routes.length; routeIndex++) {
|
|
114
215
|
const r = routes[routeIndex];
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
}
|
|
119
230
|
break;
|
|
120
231
|
}
|
|
121
232
|
}
|
|
@@ -133,19 +244,14 @@ class Walk {
|
|
|
133
244
|
// path is still relative to it. /alone/skip must not be offered to the app as /skip
|
|
134
245
|
if (req._stack !== null && req._stack.length > 0) {
|
|
135
246
|
req._stack.length = 0;
|
|
136
|
-
req.
|
|
137
|
-
req
|
|
138
|
-
req.url = req._originalPath + req.urlQuery;
|
|
139
|
-
req._opPath =
|
|
140
|
-
req.endsWithSlash && req._originalPath !== "/" && !router.get("strict routing")
|
|
141
|
-
? req._originalPath.slice(0, -1)
|
|
142
|
-
: req._originalPath;
|
|
143
|
-
req._lastUrl = req.url;
|
|
247
|
+
req._consumed = 0;
|
|
248
|
+
setMountedPath(req);
|
|
144
249
|
}
|
|
145
250
|
// an error out of a mount is attributed to the mount, so error handlers declared before
|
|
146
251
|
// it do not catch it, as in ordinary dispatch
|
|
147
252
|
if (req._error && this.skipUntil && this.skipUntil.keepMount && this.skipUntil.routeKey > req._errorKey) {
|
|
148
253
|
req._errorKey = this.skipUntil.routeKey;
|
|
254
|
+
req._errorGroup = this.skipUntil.group;
|
|
149
255
|
}
|
|
150
256
|
this.routes = router._routes;
|
|
151
257
|
this.skipCheck = false;
|
|
@@ -191,14 +297,8 @@ class Walk {
|
|
|
191
297
|
this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
|
|
192
298
|
if (req._stack !== null && req._stack.length > 0) {
|
|
193
299
|
req._stack.length = 0;
|
|
194
|
-
req.
|
|
195
|
-
req
|
|
196
|
-
req.url = req._originalPath + req.urlQuery;
|
|
197
|
-
req._opPath =
|
|
198
|
-
req.endsWithSlash && req._originalPath !== "/" && !router.get("strict routing")
|
|
199
|
-
? req._originalPath.slice(0, -1)
|
|
200
|
-
: req._originalPath;
|
|
201
|
-
req._lastUrl = req.url;
|
|
300
|
+
req._consumed = 0;
|
|
301
|
+
setMountedPath(req);
|
|
202
302
|
}
|
|
203
303
|
this.routes = router._routes;
|
|
204
304
|
this.skipCheck = false;
|
|
@@ -215,37 +315,25 @@ class Walk {
|
|
|
215
315
|
runRoute(continueRoute) {
|
|
216
316
|
const req = this.req;
|
|
217
317
|
const route = this.route;
|
|
218
|
-
const router = this.router;
|
|
219
318
|
if (route.use) {
|
|
220
319
|
if (route.mountApp) {
|
|
221
320
|
// optimized chain: normal dispatch swaps req.app when it enters a mounted
|
|
222
321
|
// Application, but the compiled mount route has no callback to do it
|
|
322
|
+
rememberApp(this, route, req);
|
|
223
323
|
useApp(req, route.mountApp);
|
|
224
324
|
}
|
|
225
|
-
|
|
325
|
+
const taken = mountPrefixLength(route, req);
|
|
326
|
+
(req._stack ??= []).push(taken);
|
|
226
327
|
// a use with no path consumes nothing, so everything below would work out the values
|
|
227
328
|
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
228
329
|
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
const fullMountpath = router.getFullMountpath(req);
|
|
234
|
-
req._opPath =
|
|
235
|
-
fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, "") : req._originalPath;
|
|
236
|
-
if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
|
|
237
|
-
req._opPath = router.get("strict routing") ? req._opPath + "/" : req._opPath.slice(0, -1);
|
|
238
|
-
}
|
|
239
|
-
req.url = req._opPath + req.urlQuery;
|
|
240
|
-
req.path = req._opPath;
|
|
241
|
-
if (req._opPath === "") {
|
|
242
|
-
req.url = "/" + req.urlQuery;
|
|
243
|
-
req.path = "/";
|
|
244
|
-
}
|
|
245
|
-
req._lastUrl = req.url;
|
|
330
|
+
if (taken !== 0 || req.endsWithSlash) {
|
|
331
|
+
req._consumed += taken;
|
|
332
|
+
setMountedPath(req);
|
|
246
333
|
}
|
|
247
334
|
}
|
|
248
335
|
req.next = this.next;
|
|
336
|
+
req._leaveRoute = this.leaveRoute;
|
|
249
337
|
if (continueRoute === "route") {
|
|
250
338
|
this.step("route");
|
|
251
339
|
} else if (continueRoute) {
|
|
@@ -265,7 +353,15 @@ class Walk {
|
|
|
265
353
|
errorHop(kind, callback) {
|
|
266
354
|
const req = this.req;
|
|
267
355
|
const route = this.route;
|
|
268
|
-
|
|
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) {
|
|
269
365
|
const out = this.router._handleError(req._error, callback, req, this.res);
|
|
270
366
|
if (out instanceof Promise) {
|
|
271
367
|
// an error handler's rejected promise moves on to the next error handler, and
|
|
@@ -273,6 +369,7 @@ class Walk {
|
|
|
273
369
|
out.catch((err) => {
|
|
274
370
|
req._error = err || new Error("Rejected promise");
|
|
275
371
|
req._errorKey = route.routeKey;
|
|
372
|
+
req._errorGroup = route.group;
|
|
276
373
|
return this.step(undefined);
|
|
277
374
|
});
|
|
278
375
|
}
|
|
@@ -300,45 +397,20 @@ class Walk {
|
|
|
300
397
|
if (req.url !== req._lastUrl) {
|
|
301
398
|
req._absorbUrlRewrite();
|
|
302
399
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const strictRouting = router.get("strict routing");
|
|
308
|
-
const poppedMountpath = req._stack.length > 0 ? router.getFullMountpath(req) : EMPTY_REGEX;
|
|
309
|
-
req._opPath =
|
|
310
|
-
poppedMountpath !== EMPTY_REGEX
|
|
311
|
-
? req._originalPath.replace(poppedMountpath, "")
|
|
312
|
-
: req._originalPath;
|
|
313
|
-
if (strictRouting) {
|
|
314
|
-
if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
|
|
315
|
-
req._opPath += "/";
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
req.url = req._opPath + req.urlQuery;
|
|
319
|
-
req.path = req._opPath;
|
|
320
|
-
if (req._opPath === "") {
|
|
321
|
-
req.url = "/" + req.urlQuery;
|
|
322
|
-
req.path = "/";
|
|
323
|
-
}
|
|
324
|
-
req._lastUrl = req.url;
|
|
325
|
-
if (
|
|
326
|
-
!strictRouting &&
|
|
327
|
-
req.endsWithSlash &&
|
|
328
|
-
req._originalPath !== "/" &&
|
|
329
|
-
req._opPath[req._opPath.length - 1] === "/"
|
|
330
|
-
) {
|
|
331
|
-
req._opPath = req._opPath.slice(0, -1);
|
|
332
|
-
}
|
|
333
|
-
if (req.app.parent && route.callbacks[0]?.constructor.name === "Application") {
|
|
334
|
-
useApp(req, req.app.parent);
|
|
335
|
-
}
|
|
400
|
+
req._consumed -= req._stack.pop();
|
|
401
|
+
setMountedPath(req);
|
|
402
|
+
restoreApp(route, req);
|
|
336
403
|
}
|
|
337
404
|
if (thingamabob === "router") {
|
|
338
405
|
if (this.skipCheck) {
|
|
339
406
|
// on a compiled chain, leaving the router is what running out of chain
|
|
340
|
-
// already means: ordinary routing takes over after the mount
|
|
341
|
-
|
|
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);
|
|
342
414
|
}
|
|
343
415
|
// out of this router entirely, so whoever mounted it carries on after the
|
|
344
416
|
// mount. The app's own walk has nobody after it, and answers 404
|
|
@@ -355,6 +427,7 @@ class Walk {
|
|
|
355
427
|
} else {
|
|
356
428
|
req._error = thingamabob;
|
|
357
429
|
req._errorKey = route.routeKey;
|
|
430
|
+
req._errorGroup = route.group;
|
|
358
431
|
}
|
|
359
432
|
}
|
|
360
433
|
const kind = route.callbackKinds[this.callbackIndex];
|
|
@@ -370,6 +443,7 @@ class Walk {
|
|
|
370
443
|
}
|
|
371
444
|
if (kind === CALLBACK_ROUTER) {
|
|
372
445
|
if (callback.constructor.name === "Application") {
|
|
446
|
+
rememberApp(this, route, req);
|
|
373
447
|
useApp(req, callback);
|
|
374
448
|
}
|
|
375
449
|
const pushedParams = callback.settings.mergeParams;
|
|
@@ -379,12 +453,12 @@ class Walk {
|
|
|
379
453
|
// express restores req.params when a router hands back, so what runs after the mount
|
|
380
454
|
// sees the params it had before it
|
|
381
455
|
const parentParams = req.params;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
) {
|
|
387
|
-
req.
|
|
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();
|
|
388
462
|
}
|
|
389
463
|
callback
|
|
390
464
|
._routeRequest(req, res, 0)
|
|
@@ -396,15 +470,25 @@ class Walk {
|
|
|
396
470
|
req.params = parentParams;
|
|
397
471
|
if (req._error) {
|
|
398
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);
|
|
399
480
|
}
|
|
400
|
-
|
|
401
|
-
if (
|
|
481
|
+
const childMethods = req._matchedMethods;
|
|
482
|
+
if (parentMethods !== null) {
|
|
483
|
+
req._matchedMethods = parentMethods;
|
|
484
|
+
}
|
|
485
|
+
if (req._isOptions && childMethods.size) {
|
|
402
486
|
// OPTIONS routing is different, it stops in the router if matched.
|
|
403
487
|
// Express answers as the router hands back, so a throw while answering,
|
|
404
488
|
// a head already written being the way, walks on to later error handlers
|
|
405
489
|
if (!req._error) {
|
|
406
490
|
try {
|
|
407
|
-
router._sendOptionsReply(req, res);
|
|
491
|
+
router._sendOptionsReply(req, res, childMethods);
|
|
408
492
|
return this.resolve(true);
|
|
409
493
|
} catch (err) {
|
|
410
494
|
return this.step(err);
|
|
@@ -444,12 +528,14 @@ class Walk {
|
|
|
444
528
|
out.catch((err) => {
|
|
445
529
|
req._error = err || new Error("Rejected promise");
|
|
446
530
|
req._errorKey = route.routeKey;
|
|
531
|
+
req._errorGroup = route.group;
|
|
447
532
|
return this.step(undefined);
|
|
448
533
|
});
|
|
449
534
|
}
|
|
450
535
|
} catch (err) {
|
|
451
536
|
req._error = err;
|
|
452
537
|
req._errorKey = route.routeKey;
|
|
538
|
+
req._errorGroup = route.group;
|
|
453
539
|
return this.step(undefined);
|
|
454
540
|
}
|
|
455
541
|
}
|
|
@@ -512,22 +598,62 @@ function nativeFail(err) {
|
|
|
512
598
|
}
|
|
513
599
|
|
|
514
600
|
/**
|
|
515
|
-
*
|
|
516
|
-
*
|
|
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
|
|
517
603
|
* everything leaves "", where the pattern was matched against "/".
|
|
518
604
|
*
|
|
519
|
-
*
|
|
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
|
+
*
|
|
520
610
|
* @param {any} route
|
|
521
611
|
* @param {any} req
|
|
522
|
-
* @returns {
|
|
612
|
+
* @returns {number}
|
|
523
613
|
*/
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
614
|
+
function mountPrefixLength(route, req) {
|
|
615
|
+
const path = req._opPath;
|
|
616
|
+
if (typeof route.pattern === "string") {
|
|
617
|
+
return route.pattern.length;
|
|
528
618
|
}
|
|
529
|
-
const matched = route.pattern.exec(
|
|
530
|
-
return matched ?
|
|
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);
|
|
531
657
|
}
|
|
532
658
|
|
|
533
659
|
/**
|
|
@@ -627,7 +753,7 @@ function adoptPlainRequest(req, router) {
|
|
|
627
753
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
628
754
|
req._originalPath = path;
|
|
629
755
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
630
|
-
req._opPath =
|
|
756
|
+
req._opPath = path;
|
|
631
757
|
req._lastUrl = req.url;
|
|
632
758
|
req._isOptions = req.method === "OPTIONS";
|
|
633
759
|
req._isHead = req.method === "HEAD";
|
|
@@ -635,7 +761,7 @@ function adoptPlainRequest(req, router) {
|
|
|
635
761
|
// null, not fresh arrays: the push sites materialize them on the first mount, and most
|
|
636
762
|
// requests never see one, same as the Request constructor
|
|
637
763
|
req._stack = null;
|
|
638
|
-
req.
|
|
764
|
+
req._consumed = 0;
|
|
639
765
|
req._paramStack = null;
|
|
640
766
|
req._matchedMethods = req._isOptions ? new Set() : null;
|
|
641
767
|
req.routeCount = 1;
|
|
@@ -672,9 +798,6 @@ function onNativeAborted() {
|
|
|
672
798
|
response.socket?.emit("error", err);
|
|
673
799
|
}
|
|
674
800
|
|
|
675
|
-
/**
|
|
676
|
-
*
|
|
677
|
-
*/
|
|
678
801
|
/**
|
|
679
802
|
* The per-request constants of a fully literal native registration. µWS matched the URL byte for
|
|
680
803
|
* byte against this exact pattern and dispatches by method, so the request constructor can take
|
|
@@ -682,15 +805,14 @@ function onNativeAborted() {
|
|
|
682
805
|
*
|
|
683
806
|
* @param {string} path the registered pattern, which is what getUrl() would have answered
|
|
684
807
|
* @param {string} method uppercase, fixed by which uWS verb the registration used
|
|
685
|
-
* @param {boolean} strict the owner's strict routing, frozen here like the twin registration is
|
|
686
808
|
*/
|
|
687
|
-
function nativePreset(path, method
|
|
809
|
+
function nativePreset(path, method) {
|
|
688
810
|
const endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
689
811
|
return {
|
|
690
812
|
path,
|
|
691
813
|
method,
|
|
692
814
|
endsWithSlash,
|
|
693
|
-
opPath:
|
|
815
|
+
opPath: path,
|
|
694
816
|
isOptions: method === "OPTIONS",
|
|
695
817
|
isHead: method === "HEAD",
|
|
696
818
|
// set at registration when the whole chain provably never reads a header, or never
|
|
@@ -751,6 +873,138 @@ const CALLBACK_ROUTER = 2;
|
|
|
751
873
|
* @param {any} req
|
|
752
874
|
* @param {any} app
|
|
753
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
|
+
*/
|
|
754
1008
|
function useApp(req, app) {
|
|
755
1009
|
req.app = app;
|
|
756
1010
|
if (req.res) {
|
|
@@ -775,7 +1029,8 @@ function useApp(req, app) {
|
|
|
775
1029
|
const methods = ["all", ...METHODS.filter((method) => method !== "GET").map((method) => method.toLowerCase())];
|
|
776
1030
|
const supportedUwsMethods = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]);
|
|
777
1031
|
|
|
778
|
-
|
|
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;
|
|
779
1034
|
|
|
780
1035
|
// Internals here are _underscore and not #private: a callable router is a function with the
|
|
781
1036
|
// router's properties copied onto it, and a # field cannot be copied, so #routes would throw
|
|
@@ -817,10 +1072,24 @@ function callablePrototypeFor(classPrototype) {
|
|
|
817
1072
|
* The default error page, which is the one Express produces: the stack in a pre, and nothing else.
|
|
818
1073
|
* What reaches it has already been redacted when the environment calls for it.
|
|
819
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
|
+
*
|
|
820
1081
|
* @param {any} err
|
|
821
1082
|
* @returns {string}
|
|
822
1083
|
*/
|
|
823
1084
|
function generateErrorPageHtml(err) {
|
|
1085
|
+
const text = String(err?.stack ?? err)
|
|
1086
|
+
.replace(/&/g, "&")
|
|
1087
|
+
.replace(/</g, "<")
|
|
1088
|
+
.replace(/>/g, ">")
|
|
1089
|
+
.replace(/"/g, """)
|
|
1090
|
+
.replace(/'/g, "'")
|
|
1091
|
+
.replace(/\n/g, "<br>")
|
|
1092
|
+
.replace(/ {2}/g, " ");
|
|
824
1093
|
return (
|
|
825
1094
|
`<!DOCTYPE html>\n` +
|
|
826
1095
|
`<html lang="en">\n` +
|
|
@@ -829,19 +1098,66 @@ function generateErrorPageHtml(err) {
|
|
|
829
1098
|
`<title>Error</title>\n` +
|
|
830
1099
|
`</head>\n` +
|
|
831
1100
|
`<body>\n` +
|
|
832
|
-
`<pre>${
|
|
1101
|
+
`<pre>${text}</pre>\n` +
|
|
833
1102
|
`</body>\n` +
|
|
834
1103
|
`</html>\n`
|
|
835
1104
|
);
|
|
836
1105
|
}
|
|
837
1106
|
|
|
838
1107
|
module.exports = class Router extends EventEmitter {
|
|
1108
|
+
/**
|
|
1109
|
+
* The router or application this one is mounted on, undefined until it is.
|
|
1110
|
+
* @type {any}
|
|
1111
|
+
*/
|
|
839
1112
|
parent;
|
|
840
1113
|
|
|
1114
|
+
/**
|
|
1115
|
+
* Whether listen() has run, after which a new route can no longer reach uWS.
|
|
1116
|
+
* @type {boolean|undefined}
|
|
1117
|
+
*/
|
|
841
1118
|
listenCalled;
|
|
842
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* The uWS app routes are registered on.
|
|
1122
|
+
*
|
|
1123
|
+
* Typed loosely on purpose: only an Application owns one, and the callers that reach for
|
|
1124
|
+
* it have already established that, so the honest `TemplatedApp|undefined` would only add
|
|
1125
|
+
* casts where the guard already is.
|
|
1126
|
+
*
|
|
1127
|
+
* @type {any}
|
|
1128
|
+
*/
|
|
843
1129
|
uwsApp;
|
|
844
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
|
+
|
|
845
1161
|
/**
|
|
846
1162
|
* @param {object} [settings] router options. caseSensitive and strict are accepted under the
|
|
847
1163
|
* names Express's Router takes, and stored under the setting names the rest of the code reads
|
|
@@ -852,6 +1168,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
852
1168
|
this._paramCallbacks = new Map();
|
|
853
1169
|
this._mountpathCache = new Map();
|
|
854
1170
|
this._routes = [];
|
|
1171
|
+
// websocket routes, kept apart from the HTTP ones: µWS serves them itself and listen()
|
|
1172
|
+
// hands them over whole, mount paths and all
|
|
1173
|
+
/** @type {any[]|null} */
|
|
1174
|
+
this._wsRoutes = null;
|
|
855
1175
|
// the native presets allowed to skip the header copy, so a late middleware or an etag
|
|
856
1176
|
// arriving after listen can take the permission back; null until one is granted
|
|
857
1177
|
/** @type {Set<any>|null} */
|
|
@@ -970,6 +1290,53 @@ module.exports = class Router extends EventEmitter {
|
|
|
970
1290
|
return this.createRoute("GET", path, this, ...callbacks);
|
|
971
1291
|
}
|
|
972
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
|
+
|
|
973
1340
|
/**
|
|
974
1341
|
* The pattern matching everything the mounts on this request have consumed so far, which is
|
|
975
1342
|
* what a nested router strips off the path before matching against it. Cached per stack, since
|
|
@@ -1000,7 +1367,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1000
1367
|
const stackPattern = fullStack.includes(":")
|
|
1001
1368
|
? fullStack.replace(/(\\?):(\w+)/g, (whole, escaped, name, at) => (escaped ? whole : ":m" + at))
|
|
1002
1369
|
: fullStack;
|
|
1003
|
-
|
|
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);
|
|
1004
1376
|
this._mountpathCache.set(fullStack, fullMountpath);
|
|
1005
1377
|
}
|
|
1006
1378
|
return fullMountpath;
|
|
@@ -1016,18 +1388,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1016
1388
|
* @returns {boolean}
|
|
1017
1389
|
*/
|
|
1018
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
|
|
1019
1393
|
let path = req._opPath;
|
|
1020
1394
|
let pattern = route.pattern;
|
|
1021
|
-
|
|
1022
|
-
if (route.userRegexp) {
|
|
1023
|
-
// a RegExp the application wrote is matched against the path as it arrived: express
|
|
1024
|
-
// relaxes a trailing slash only for the paths it compiled itself
|
|
1025
|
-
if (req.endsWithSlash && !path.endsWith("/")) {
|
|
1026
|
-
path += "/";
|
|
1027
|
-
}
|
|
1028
|
-
} else if (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
|
|
1029
|
-
path = path.slice(0, -1);
|
|
1030
|
-
}
|
|
1031
1395
|
// the line above turns the root path into the empty string, which no pattern is written
|
|
1032
1396
|
// against. A regex route was tested against it and app.get("*path") answered every request
|
|
1033
1397
|
// but "/"
|
|
@@ -1039,11 +1403,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
1039
1403
|
if (pattern === "/*") {
|
|
1040
1404
|
return true;
|
|
1041
1405
|
}
|
|
1042
|
-
if (!this.
|
|
1406
|
+
if (!this._caseSensitive()) {
|
|
1043
1407
|
path = path.toLowerCase();
|
|
1044
1408
|
pattern = pattern.toLowerCase();
|
|
1045
1409
|
}
|
|
1046
|
-
|
|
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
|
+
);
|
|
1047
1422
|
}
|
|
1048
1423
|
if (pattern === EMPTY_REGEX) {
|
|
1049
1424
|
return true;
|
|
@@ -1083,12 +1458,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1083
1458
|
// a mount always drops it, strict routing or not: strictness is about the end of a
|
|
1084
1459
|
// path, and a mount has none. Express registers its use layers with strict off
|
|
1085
1460
|
if (
|
|
1086
|
-
(method === "USE" || !this.
|
|
1461
|
+
(method === "USE" || !this._strictRouting()) &&
|
|
1087
1462
|
typeof path === "string" &&
|
|
1088
1463
|
path.endsWith("/") &&
|
|
1089
1464
|
path !== "/"
|
|
1090
1465
|
) {
|
|
1091
|
-
|
|
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(/\/+$/, "");
|
|
1092
1470
|
}
|
|
1093
1471
|
if (path === "*") {
|
|
1094
1472
|
path = "/{*splat}";
|
|
@@ -1098,9 +1476,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1098
1476
|
path,
|
|
1099
1477
|
pattern:
|
|
1100
1478
|
method === "USE" || needsConversionToRegex(path)
|
|
1101
|
-
?
|
|
1102
|
-
// default parameter would silently turn back into case-sensitive
|
|
1103
|
-
patternToRegex(path, method === "USE", Boolean(this.get("case sensitive routing")))
|
|
1479
|
+
? patternToRegex(path, method === "USE", this._caseSensitive(), this._strictRouting())
|
|
1104
1480
|
: path,
|
|
1105
1481
|
callbacks,
|
|
1106
1482
|
// instanceof walks a prototype chain and length is a property load, and both used
|
|
@@ -1112,12 +1488,23 @@ module.exports = class Router extends EventEmitter {
|
|
|
1112
1488
|
? CALLBACK_ERROR
|
|
1113
1489
|
: CALLBACK_PLAIN
|
|
1114
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,
|
|
1115
1499
|
// a mount written as a RegExp matches a piece of path that is not known until a
|
|
1116
1500
|
// request comes in, so its stack entry cannot be the path itself
|
|
1117
1501
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
1118
1502
|
// written by the application, so express matches it as it stands
|
|
1119
1503
|
userRegexp: path instanceof RegExp,
|
|
1120
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,
|
|
1121
1508
|
// the router this was registered on. Ordinary dispatch is done by that router, so
|
|
1122
1509
|
// it could ask itself, but an optimized chain is walked by the app whatever it
|
|
1123
1510
|
// contains, and param() callbacks belong to the router that declared them
|
|
@@ -1175,8 +1562,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1175
1562
|
// so the text comparisons below run on the folded form. µWS itself still matches bytes:
|
|
1176
1563
|
// a request in the registered case takes the chain, any other case takes the fallback,
|
|
1177
1564
|
// and both answer as express would as long as the chain agrees with registration order
|
|
1178
|
-
const caseSensitive =
|
|
1565
|
+
const caseSensitive = this._caseSensitive();
|
|
1179
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;
|
|
1180
1571
|
|
|
1181
1572
|
for (let i = 0; i < routes.length; i++) {
|
|
1182
1573
|
const r = routes[i];
|
|
@@ -1190,6 +1581,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
1190
1581
|
if (!r.all && r.method !== route.method) {
|
|
1191
1582
|
// check if the methods are compatible (GET and HEAD)
|
|
1192
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
|
+
}
|
|
1193
1596
|
continue;
|
|
1194
1597
|
}
|
|
1195
1598
|
}
|
|
@@ -1219,9 +1622,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
1219
1622
|
}
|
|
1220
1623
|
|
|
1221
1624
|
// check if the paths match. A route with parameters is excluded from the text test:
|
|
1222
|
-
// 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.
|
|
1223
1629
|
if (
|
|
1224
|
-
(r.pattern instanceof RegExp &&
|
|
1630
|
+
(r.pattern instanceof RegExp &&
|
|
1631
|
+
(!withParams || r.use) &&
|
|
1632
|
+
(r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
|
|
1225
1633
|
(typeof r.pattern === "string" &&
|
|
1226
1634
|
(r.pattern === route.path ||
|
|
1227
1635
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
@@ -1259,18 +1667,26 @@ module.exports = class Router extends EventEmitter {
|
|
|
1259
1667
|
continue;
|
|
1260
1668
|
}
|
|
1261
1669
|
// otherwise the two overlap only where µWS itself hands the request to the earlier,
|
|
1262
|
-
// more specific registration, so this chain never sees those paths
|
|
1263
|
-
// about bytes, so under insensitive routing it only holds when no literal hides
|
|
1264
|
-
// behind a case difference
|
|
1670
|
+
// more specific registration, so this chain never sees those paths
|
|
1265
1671
|
if (
|
|
1266
1672
|
!r.optimizedPath ||
|
|
1267
1673
|
!uwsPrefersEarlier(r.path, route.path) ||
|
|
1268
|
-
(!caseSensitive &&
|
|
1674
|
+
(!caseSensitive && route.path !== routePathFolded)
|
|
1269
1675
|
) {
|
|
1270
1676
|
return false;
|
|
1271
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
|
+
}
|
|
1272
1687
|
}
|
|
1273
1688
|
optimizedPath.push(route);
|
|
1689
|
+
route._caseGuards = caseGuards;
|
|
1274
1690
|
|
|
1275
1691
|
return optimizedPath;
|
|
1276
1692
|
}
|
|
@@ -1302,8 +1718,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1302
1718
|
) {
|
|
1303
1719
|
let pathToMount = router._optimizeRoute(route, router._routes);
|
|
1304
1720
|
if (!pathToMount) {
|
|
1721
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
1305
1722
|
continue;
|
|
1306
1723
|
}
|
|
1724
|
+
route._walkedInto = true;
|
|
1307
1725
|
pathToMount = pathToMount.slice(0, -1);
|
|
1308
1726
|
walk(route.callbacks[0], pathPrefix + route.path, [
|
|
1309
1727
|
...chainPrefix,
|
|
@@ -1320,6 +1738,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1320
1738
|
: undefined
|
|
1321
1739
|
}
|
|
1322
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";
|
|
1323
1748
|
}
|
|
1324
1749
|
// µWS picks by specificity and Express by registration order, so the chain
|
|
1325
1750
|
// computed for whichever route µWS lands on runs everything that could have
|
|
@@ -1334,6 +1759,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1334
1759
|
) {
|
|
1335
1760
|
const leafPath = router._optimizeRoute(route, router._routes);
|
|
1336
1761
|
if (!leafPath) {
|
|
1762
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
1337
1763
|
continue;
|
|
1338
1764
|
}
|
|
1339
1765
|
// param route earlier in the same router would steal this static path
|
|
@@ -1346,6 +1772,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1346
1772
|
shadow.path !== route.path &&
|
|
1347
1773
|
shadow.pattern instanceof RegExp
|
|
1348
1774
|
) {
|
|
1775
|
+
route._whyGeneric = `the parameter route ${shadow.path} is written before it`;
|
|
1349
1776
|
continue;
|
|
1350
1777
|
}
|
|
1351
1778
|
}
|
|
@@ -1360,6 +1787,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
1360
1787
|
pattern: pathPrefix + route.path,
|
|
1361
1788
|
optimizedRouter: true
|
|
1362
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
|
+
}
|
|
1363
1795
|
this._registerUwsRoute(registered, chain);
|
|
1364
1796
|
// the chain holds the original object, so the request-time guard has to
|
|
1365
1797
|
// find the computed fields there, or a mounted param route extracts its
|
|
@@ -1367,9 +1799,19 @@ module.exports = class Router extends EventEmitter {
|
|
|
1367
1799
|
// adds no parameter of its own
|
|
1368
1800
|
route.optimizedParams = registered.optimizedParams;
|
|
1369
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;
|
|
1370
1805
|
} else {
|
|
1371
1806
|
this._registerUwsRoute(route, chain);
|
|
1372
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";
|
|
1373
1815
|
}
|
|
1374
1816
|
}
|
|
1375
1817
|
};
|
|
@@ -1393,7 +1835,6 @@ module.exports = class Router extends EventEmitter {
|
|
|
1393
1835
|
const request = new this._request(req, res, this, preset, skipHolder);
|
|
1394
1836
|
const response = new this._response(res, request, this);
|
|
1395
1837
|
request.res = response;
|
|
1396
|
-
response.req = request;
|
|
1397
1838
|
|
|
1398
1839
|
return request;
|
|
1399
1840
|
}
|
|
@@ -1424,7 +1865,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1424
1865
|
*/
|
|
1425
1866
|
_isFollowedByAnOverlap(route, routes) {
|
|
1426
1867
|
// folded under insensitive routing, where a case variant answers the same requests
|
|
1427
|
-
const caseSensitive =
|
|
1868
|
+
const caseSensitive = this._caseSensitive();
|
|
1428
1869
|
const routePath = caseSensitive ? route.path : route.path.toLowerCase();
|
|
1429
1870
|
for (let i = routes.length - 1; i >= 0; i--) {
|
|
1430
1871
|
const later = routes[i];
|
|
@@ -1466,6 +1907,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
1466
1907
|
if (route.path.includes(":")) {
|
|
1467
1908
|
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
1468
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;
|
|
1469
1918
|
const makeHandler = (chain, preset, skips) => {
|
|
1470
1919
|
// the mutable object a granted skip lives on, so a middleware arriving after
|
|
1471
1920
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
@@ -1487,6 +1936,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1487
1936
|
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
1488
1937
|
// microtask, which is where the await used to resume, so the visible order holds
|
|
1489
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
|
+
}
|
|
1490
1946
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
1491
1947
|
const response = request.res;
|
|
1492
1948
|
if (optimizedParams) {
|
|
@@ -1527,7 +1983,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1527
1983
|
// the route's own router decides, not the app running the registration: a router created
|
|
1528
1984
|
// with { strict: true } and mounted on an app without it does not answer /things/, and
|
|
1529
1985
|
// registering that path here is the only way it could
|
|
1530
|
-
const strictHere =
|
|
1986
|
+
const strictHere = (route.owner ?? this)._strictRouting();
|
|
1531
1987
|
|
|
1532
1988
|
// Whether requests served by this registration may skip the header copy: GET and its
|
|
1533
1989
|
// HEAD twins only, the app must not compute etags (send would consult freshness
|
|
@@ -1553,7 +2009,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1553
2009
|
}
|
|
1554
2010
|
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
1555
2011
|
const makePreset = (path, method, skips) => {
|
|
1556
|
-
const preset = nativePreset(path, method
|
|
2012
|
+
const preset = nativePreset(path, method);
|
|
1557
2013
|
if (skips.skipHeaders || skips.skipQuery) {
|
|
1558
2014
|
preset.skipHeaders = skips.skipHeaders;
|
|
1559
2015
|
preset.skipQuery = skips.skipQuery;
|
|
@@ -1580,6 +2036,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1580
2036
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
1581
2037
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
1582
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 &&
|
|
1583
2042
|
!resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
|
|
1584
2043
|
this.get("declarative responses") // must have declarative responses enabled
|
|
1585
2044
|
) {
|
|
@@ -1591,6 +2050,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
1591
2050
|
replacedPath = route.path.replace(regExParam, ":x");
|
|
1592
2051
|
}
|
|
1593
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
|
+
|
|
1594
2065
|
this.uwsApp[method](replacedPath, fn);
|
|
1595
2066
|
if (!strictHere && route.path[route.path.length - 1] !== "/") {
|
|
1596
2067
|
// a declarative response answers the twin as itself; a preset handler cannot be
|
|
@@ -1718,10 +2189,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
1718
2189
|
// asking for each name in turn rather than walking the groups object, which is a
|
|
1719
2190
|
// null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
|
|
1720
2191
|
// worked out when the pattern was compiled instead of searching an array for it
|
|
1721
|
-
const { paramNames, isWildcard } = meta;
|
|
2192
|
+
const { paramNames, outputNames, isWildcard } = meta;
|
|
1722
2193
|
for (let i = 0, len = paramNames.length; i < len; i++) {
|
|
1723
|
-
const name =
|
|
1724
|
-
const value = groups[
|
|
2194
|
+
const name = outputNames[i];
|
|
2195
|
+
const value = groups[paramNames[i]];
|
|
1725
2196
|
// an optional group that did not match is absent in v5, not present as undefined
|
|
1726
2197
|
if (value === undefined) {
|
|
1727
2198
|
continue;
|
|
@@ -1755,36 +2226,29 @@ module.exports = class Router extends EventEmitter {
|
|
|
1755
2226
|
req.params[name] = decodeParam(req.optimizedParams[name]);
|
|
1756
2227
|
}
|
|
1757
2228
|
} catch (err) {
|
|
1758
|
-
req
|
|
1759
|
-
req._errorKey = route.routeKey;
|
|
2229
|
+
raiseDecodeFailure(req, route, err);
|
|
1760
2230
|
return "route";
|
|
1761
2231
|
}
|
|
1762
2232
|
} else if (route.complex) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const fullMountpath = this.getFullMountpath(req);
|
|
1766
|
-
if (fullMountpath !== EMPTY_REGEX) {
|
|
1767
|
-
path = path.replace(fullMountpath, "");
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
2233
|
+
// the path with the mounts taken off, which is what _opPath is
|
|
2234
|
+
const path = req._opPath;
|
|
1770
2235
|
try {
|
|
1771
2236
|
req.params = this._extractParams(route.pattern, path);
|
|
1772
2237
|
} catch (err) {
|
|
1773
2238
|
// a parameter that will not decode. Express throws out of the match and lets the
|
|
1774
2239
|
// error reach the error handler, which answers 400, so the route is skipped rather
|
|
1775
2240
|
// than run with a value nobody can read.
|
|
1776
|
-
req
|
|
1777
|
-
req._errorKey = route.routeKey;
|
|
2241
|
+
raiseDecodeFailure(req, route, err);
|
|
1778
2242
|
return "route";
|
|
1779
2243
|
}
|
|
1780
|
-
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
2244
|
+
if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
|
|
1781
2245
|
req.params = mergeParams(req.params, req._paramStack);
|
|
1782
2246
|
}
|
|
1783
2247
|
} else {
|
|
1784
2248
|
// express 5 gives every matched route null-prototype params; only a pathless
|
|
1785
2249
|
// middleware layer keeps the plain object, as its router hands one to fast_slash
|
|
1786
2250
|
req.params = route.use && route.path === "" ? {} : Object.create(null);
|
|
1787
|
-
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
2251
|
+
if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
|
|
1788
2252
|
req.params = mergeParams(req.params, req._paramStack);
|
|
1789
2253
|
}
|
|
1790
2254
|
}
|
|
@@ -1798,6 +2262,27 @@ module.exports = class Router extends EventEmitter {
|
|
|
1798
2262
|
return true;
|
|
1799
2263
|
}
|
|
1800
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
|
+
|
|
1801
2286
|
/**
|
|
1802
2287
|
* Runs the app.param() callbacks for the parameters this route matched, and says whether the
|
|
1803
2288
|
* route may run.
|
|
@@ -1843,6 +2328,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1843
2328
|
if (err !== "route") {
|
|
1844
2329
|
req._error = err;
|
|
1845
2330
|
req._errorKey = route.routeKey;
|
|
2331
|
+
req._errorGroup = route.group;
|
|
1846
2332
|
}
|
|
1847
2333
|
// the route is skipped either way: an error carries on to the error handlers
|
|
1848
2334
|
return resolve("route");
|
|
@@ -1974,6 +2460,38 @@ module.exports = class Router extends EventEmitter {
|
|
|
1974
2460
|
return this;
|
|
1975
2461
|
}
|
|
1976
2462
|
|
|
2463
|
+
/**
|
|
2464
|
+
* Registers a websocket route, which µWS serves itself.
|
|
2465
|
+
*
|
|
2466
|
+
* The behavior is µWS's, settings and socket handlers alike, plus one addition: an
|
|
2467
|
+
* `upgrade(req, res)` of this project's own shape, which runs before the handshake with a
|
|
2468
|
+
* real request and response. Answering with the response declines the socket, which is how
|
|
2469
|
+
* a check refuses one; returning a promise holds the handshake until it settles.
|
|
2470
|
+
*
|
|
2471
|
+
* The request lives as long as the socket and reaches every handler as `ws.req`, so what
|
|
2472
|
+
* the upgrade learned about the client, and anything it hangs on the request, is there when
|
|
2473
|
+
* a message arrives.
|
|
2474
|
+
*
|
|
2475
|
+
* @example
|
|
2476
|
+
* app.ws("/room/:id", {
|
|
2477
|
+
* upgrade(req, res) {
|
|
2478
|
+
* if (!req.query.token) return res.sendStatus(401);
|
|
2479
|
+
* req.room = req.params.id;
|
|
2480
|
+
* },
|
|
2481
|
+
* open(ws) { ws.subscribe(ws.req.room); },
|
|
2482
|
+
* message(ws, message, isBinary) { ws.publish(ws.req.room, message, isBinary); }
|
|
2483
|
+
* });
|
|
2484
|
+
*
|
|
2485
|
+
* @param {string} path a literal path, or one whose parameters are whole segments
|
|
2486
|
+
* @param {object} behavior µWS's WebSocketBehavior, plus the optional `upgrade` above
|
|
2487
|
+
* @returns {this}
|
|
2488
|
+
*/
|
|
2489
|
+
ws(path, behavior) {
|
|
2490
|
+
checkBehavior(path, behavior);
|
|
2491
|
+
(this._wsRoutes ??= []).push({ path, behavior, owner: this });
|
|
2492
|
+
return this;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
1977
2495
|
/**
|
|
1978
2496
|
* A builder for one path, so the path is written once and the verbs chain off it.
|
|
1979
2497
|
*
|
|
@@ -1984,15 +2502,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
1984
2502
|
* @returns {object} an object with one method per HTTP verb, each returning it again
|
|
1985
2503
|
*/
|
|
1986
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;
|
|
1987
2508
|
const fns = new NullObject();
|
|
1988
|
-
|
|
1989
|
-
|
|
2509
|
+
const inGroup = (method, callbacks) => {
|
|
2510
|
+
this._pendingGroup = group;
|
|
2511
|
+
try {
|
|
1990
2512
|
return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
return this.createRoute("GET", path, /** @type {any} */ (fns), ...callbacks);
|
|
2513
|
+
} finally {
|
|
2514
|
+
this._pendingGroup = undefined;
|
|
2515
|
+
}
|
|
1995
2516
|
};
|
|
2517
|
+
for (const method of methods) {
|
|
2518
|
+
fns[method] = (...callbacks) => inGroup(method, callbacks);
|
|
2519
|
+
}
|
|
2520
|
+
fns.get = (...callbacks) => inGroup("GET", callbacks);
|
|
1996
2521
|
return fns;
|
|
1997
2522
|
}
|
|
1998
2523
|
|
|
@@ -2021,14 +2546,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
2021
2546
|
*
|
|
2022
2547
|
* @param {any} request
|
|
2023
2548
|
* @param {any} response
|
|
2549
|
+
* @param {Set<string>} methods the verbs the answering router knows, which are its own
|
|
2024
2550
|
*/
|
|
2025
|
-
_sendOptionsReply(request, response) {
|
|
2551
|
+
_sendOptionsReply(request, response, methods) {
|
|
2026
2552
|
if (response._headWritten) {
|
|
2027
2553
|
throw new Error("Cannot set headers after they are sent to the client");
|
|
2028
2554
|
}
|
|
2029
2555
|
// Express 5 sorts the methods and joins them with ", ", so the header reads the same
|
|
2030
2556
|
// regardless of the order the routes happened to be registered in
|
|
2031
|
-
const allowedMethods = Array.from(
|
|
2557
|
+
const allowedMethods = Array.from(methods).sort().join(", ");
|
|
2032
2558
|
response.setHeader("Allow", allowedMethods);
|
|
2033
2559
|
// the router package answers this one itself, with a plain-text body, the nosniff
|
|
2034
2560
|
// header and end() rather than send(), so no ETag comes with it
|
|
@@ -2051,7 +2577,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
2051
2577
|
}
|
|
2052
2578
|
if (request._isOptions && request._matchedMethods.size > 0) {
|
|
2053
2579
|
try {
|
|
2054
|
-
this._sendOptionsReply(request, response);
|
|
2580
|
+
this._sendOptionsReply(request, response, request._matchedMethods);
|
|
2055
2581
|
} catch (err) {
|
|
2056
2582
|
// a head already written: the error answers instead, as express's does
|
|
2057
2583
|
this._handleError(err, null, request, response);
|