fulmine.js 5.0.0-rc.1
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/EXPRESS_LICENSE +26 -0
- package/LICENSE +202 -0
- package/NOTICE +38 -0
- package/README.md +469 -0
- package/package.json +165 -0
- package/src/application.js +561 -0
- package/src/cli.js +369 -0
- package/src/declarative.js +768 -0
- package/src/index.js +71 -0
- package/src/middlewares.js +636 -0
- package/src/node-shim.js +400 -0
- package/src/request.js +807 -0
- package/src/response.js +1360 -0
- package/src/router.js +1240 -0
- package/src/types.d.ts +62 -0
- package/src/utils.js +993 -0
- package/src/view.js +172 -0
- package/src/worker.js +38 -0
package/src/router.js
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
patternToRegex,
|
|
20
|
+
getPatternMeta,
|
|
21
|
+
decodeParam,
|
|
22
|
+
needsConversionToRegex,
|
|
23
|
+
findIndexStartingFrom,
|
|
24
|
+
canBeOptimized,
|
|
25
|
+
canBeOptimizedWithParams,
|
|
26
|
+
pathsCanOverlap,
|
|
27
|
+
NullObject,
|
|
28
|
+
EMPTY_REGEX
|
|
29
|
+
} = require("./utils.js");
|
|
30
|
+
const Response = require("./response.js");
|
|
31
|
+
const Request = require("./request.js");
|
|
32
|
+
const { EventEmitter } = require("tseep");
|
|
33
|
+
const compileDeclarative = require("./declarative.js");
|
|
34
|
+
const statuses = require("statuses");
|
|
35
|
+
const { METHODS } = require("http");
|
|
36
|
+
const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
|
|
37
|
+
|
|
38
|
+
const resCodes = {},
|
|
39
|
+
resDecMethods = ["set", "setHeader", "header", "send", "end", "append", "status"];
|
|
40
|
+
for (const method of resDecMethods) {
|
|
41
|
+
resCodes[method] = Response.prototype[method].toString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let routeKey = 0;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Hands the request and the response to the app about to handle them, so that a mounted sub-app's
|
|
48
|
+
* settings decide what its responses do. Express re-parents both objects for the same reason.
|
|
49
|
+
*
|
|
50
|
+
* @param {any} req
|
|
51
|
+
* @param {any} app
|
|
52
|
+
*/
|
|
53
|
+
function useApp(req, app) {
|
|
54
|
+
req.app = app;
|
|
55
|
+
if (req.res) {
|
|
56
|
+
req.res.app = app;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Every verb node knows about, which is the list the methods package hands Express, and "all" on
|
|
61
|
+
// top of it. Taken from node rather than written out: the written out one was missing acl, bind,
|
|
62
|
+
// link, rebind, source, unbind, unlink and unlock, and had four of the others twice.
|
|
63
|
+
//
|
|
64
|
+
// GET is left out on purpose. get() is declared in the class, because it doubles as the settings
|
|
65
|
+
// reader, and the loop at the end of this file would replace it.
|
|
66
|
+
const methods = ["all", ...METHODS.filter((method) => method !== "GET").map((method) => method.toLowerCase())];
|
|
67
|
+
const supportedUwsMethods = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"]);
|
|
68
|
+
|
|
69
|
+
const regExParam = /:(\w+)/g;
|
|
70
|
+
|
|
71
|
+
// Internals here are _underscore and not #private: a callable router is a function with the
|
|
72
|
+
// router's properties copied onto it, and a # field cannot be copied, so #routes would throw
|
|
73
|
+
// "Cannot read private member" on the first call.
|
|
74
|
+
|
|
75
|
+
// one intermediate prototype per class, built the first time a callable of that class is made
|
|
76
|
+
const callablePrototypes = new WeakMap();
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The prototype for a callable router or app: the class prototype, with apply and call put back.
|
|
80
|
+
*
|
|
81
|
+
* Setting a function's prototype to a class prototype drops Function.prototype from the chain, and
|
|
82
|
+
* node calls a request listener with handler.apply. An intermediate object, so express.application
|
|
83
|
+
* stays in the chain. constructor and bind are not restored: the code asks constructor.name, and
|
|
84
|
+
* BIND is an HTTP verb, so app.bind registers a route as it does in Express.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} classPrototype
|
|
87
|
+
* @returns {object}
|
|
88
|
+
*/
|
|
89
|
+
function callablePrototypeFor(classPrototype) {
|
|
90
|
+
let prototype = callablePrototypes.get(classPrototype);
|
|
91
|
+
if (prototype) {
|
|
92
|
+
return prototype;
|
|
93
|
+
}
|
|
94
|
+
prototype = Object.create(classPrototype);
|
|
95
|
+
for (const name of ["apply", "call", "toString"]) {
|
|
96
|
+
Object.defineProperty(prototype, name, {
|
|
97
|
+
value: /** @type {any} */ (Function.prototype)[name],
|
|
98
|
+
writable: true,
|
|
99
|
+
configurable: true,
|
|
100
|
+
enumerable: false
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
callablePrototypes.set(classPrototype, prototype);
|
|
104
|
+
return prototype;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The default error page, which is the one Express produces: the stack in a pre, and nothing else.
|
|
109
|
+
* What reaches it has already been redacted when the environment calls for it.
|
|
110
|
+
*
|
|
111
|
+
* @param {any} err
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function generateErrorPageHtml(err) {
|
|
115
|
+
return (
|
|
116
|
+
`<!DOCTYPE html>\n` +
|
|
117
|
+
`<html lang="en">\n` +
|
|
118
|
+
`<head>\n` +
|
|
119
|
+
`<meta charset="utf-8">\n` +
|
|
120
|
+
`<title>Error</title>\n` +
|
|
121
|
+
`</head>\n` +
|
|
122
|
+
`<body>\n` +
|
|
123
|
+
`<pre>${err?.stack ?? err}</pre>\n` +
|
|
124
|
+
`</body>\n` +
|
|
125
|
+
`</html>\n`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = class Router extends EventEmitter {
|
|
130
|
+
parent;
|
|
131
|
+
|
|
132
|
+
listenCalled;
|
|
133
|
+
|
|
134
|
+
uwsApp;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {object} [settings] router options. caseSensitive and strict are accepted under the
|
|
138
|
+
* names Express's Router takes, and stored under the setting names the rest of the code reads
|
|
139
|
+
*/
|
|
140
|
+
constructor(settings = {}) {
|
|
141
|
+
super();
|
|
142
|
+
|
|
143
|
+
this._paramCallbacks = new Map();
|
|
144
|
+
this._mountpathCache = new Map();
|
|
145
|
+
this._routes = [];
|
|
146
|
+
// an array when mounted on several paths at once, as Express allows
|
|
147
|
+
/** @type {string|string[]} */
|
|
148
|
+
this.mountpath = "/";
|
|
149
|
+
this.settings = settings;
|
|
150
|
+
this._request = Request;
|
|
151
|
+
this._response = Response;
|
|
152
|
+
this.request = this._request.prototype;
|
|
153
|
+
this.response = this._response.prototype;
|
|
154
|
+
|
|
155
|
+
if (typeof settings.caseSensitive !== "undefined") {
|
|
156
|
+
this.settings["case sensitive routing"] = settings.caseSensitive;
|
|
157
|
+
delete this.settings.caseSensitive;
|
|
158
|
+
}
|
|
159
|
+
if (typeof settings.strict !== "undefined") {
|
|
160
|
+
this.settings["strict routing"] = settings.strict;
|
|
161
|
+
delete this.settings.strict;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof this.settings["case sensitive routing"] === "undefined") {
|
|
165
|
+
this.settings["case sensitive routing"] = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* This router as middleware, which is what express.Router() hands back: a function carrying the
|
|
171
|
+
* router's own properties with the router's prototype behind it. The properties are the same
|
|
172
|
+
* objects, not copies, so the function and the instance are one router seen twice.
|
|
173
|
+
*
|
|
174
|
+
* @returns {any} the callable
|
|
175
|
+
*/
|
|
176
|
+
_asCallable() {
|
|
177
|
+
// handle() comes from the prototype set below, which nothing can see from here
|
|
178
|
+
const fn = /** @type {any} */ (
|
|
179
|
+
function (req, res, next) {
|
|
180
|
+
return fn.handle(req, res, next);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
Object.assign(fn, this);
|
|
184
|
+
Object.setPrototypeOf(fn, callablePrototypeFor(Object.getPrototypeOf(this)));
|
|
185
|
+
return fn;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Routes a request through this router, as Express's app.handle and router.handle do. next() is
|
|
190
|
+
* called when nothing answered, so an unmatched request goes back to whoever is running this.
|
|
191
|
+
*
|
|
192
|
+
* @param {any} req
|
|
193
|
+
* @param {any} res
|
|
194
|
+
* @param {(err?: any) => void} [next]
|
|
195
|
+
* @returns {Promise<void>}
|
|
196
|
+
*/
|
|
197
|
+
async handle(req, res, next) {
|
|
198
|
+
// a request from node's own server, which is what http.createServer(app) delivers
|
|
199
|
+
if (isNodeRequest(req)) {
|
|
200
|
+
return serveNodeRequest(this, req, /** @type {any} */ (res), next);
|
|
201
|
+
}
|
|
202
|
+
// an app taking over a request becomes that request's app, as it does when mounted, so
|
|
203
|
+
// req.app.get("view engine") inside a sub-app reads the sub-app's settings and not the
|
|
204
|
+
// settings of whatever handed the request over. A plain router is not an app and leaves it
|
|
205
|
+
// alone, which is what Express's router.handle does too.
|
|
206
|
+
if (this.constructor.name === "Application") {
|
|
207
|
+
useApp(req, this);
|
|
208
|
+
}
|
|
209
|
+
const routed = await this._routeRequest(req, res, 0);
|
|
210
|
+
if (!routed && next) {
|
|
211
|
+
next();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Two methods sharing a name, as in Express.
|
|
217
|
+
*
|
|
218
|
+
* With a string and no handlers it reads a setting, falling back to the parent router when
|
|
219
|
+
* this one does not have it. With handlers it registers a GET route. A GET route also
|
|
220
|
+
* answers HEAD.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} path setting name, or route path
|
|
223
|
+
* @param {...(Function|Array<Function>)} callbacks handlers; none means read a setting
|
|
224
|
+
* @returns {*} the setting value, or the created route
|
|
225
|
+
*/
|
|
226
|
+
get(path, ...callbacks) {
|
|
227
|
+
if (typeof path === "string" && callbacks.length === 0) {
|
|
228
|
+
const key = path;
|
|
229
|
+
const res = this.settings[key];
|
|
230
|
+
if (typeof res === "undefined" && this.parent) {
|
|
231
|
+
return this.parent.get(key);
|
|
232
|
+
} else {
|
|
233
|
+
return res;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return this.createRoute("GET", path, this, ...callbacks);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The pattern matching everything the mounts on this request have consumed so far, which is
|
|
241
|
+
* what a nested router strips off the path before matching against it. Cached per stack, since
|
|
242
|
+
* the same mount chain is walked by every request that reaches it.
|
|
243
|
+
*
|
|
244
|
+
* @param {any} req
|
|
245
|
+
* @returns {RegExp}
|
|
246
|
+
*/
|
|
247
|
+
getFullMountpath(req) {
|
|
248
|
+
// path-less app.use() pushes "", so a stack of only those joins to "" no matter how deep it is.
|
|
249
|
+
// patternToRegex("", true) is EMPTY_REGEX, so this returns exactly what the join path would,
|
|
250
|
+
// without walking the whole stack on every hop
|
|
251
|
+
if (!req._stack.length || req._stackMounted === 0) {
|
|
252
|
+
return EMPTY_REGEX;
|
|
253
|
+
}
|
|
254
|
+
const fullStack = req._stack.join("");
|
|
255
|
+
let fullMountpath = this._mountpathCache.get(fullStack);
|
|
256
|
+
if (!fullMountpath) {
|
|
257
|
+
fullMountpath = patternToRegex(fullStack, true);
|
|
258
|
+
this._mountpathCache.set(fullStack, fullMountpath);
|
|
259
|
+
}
|
|
260
|
+
return fullMountpath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Whether a route's path matches this request. A plain string compares directly, which is what
|
|
265
|
+
* makes a route eligible for the native router; anything carrying a parameter or a wildcard was
|
|
266
|
+
* turned into a regular expression when it was registered.
|
|
267
|
+
*
|
|
268
|
+
* @param {any} route
|
|
269
|
+
* @param {any} req
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
_pathMatches(route, req) {
|
|
273
|
+
let path = req._opPath;
|
|
274
|
+
let pattern = route.pattern;
|
|
275
|
+
|
|
276
|
+
if (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
|
|
277
|
+
path = path.slice(0, -1);
|
|
278
|
+
}
|
|
279
|
+
// the line above turns the root path into the empty string, which no pattern is written
|
|
280
|
+
// against. A regex route was tested against it and app.get("*path") answered every request
|
|
281
|
+
// but "/"
|
|
282
|
+
if (path === "") {
|
|
283
|
+
path = "/";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (typeof pattern === "string") {
|
|
287
|
+
if (pattern === "/*") {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
if (!this.get("case sensitive routing")) {
|
|
291
|
+
path = path.toLowerCase();
|
|
292
|
+
pattern = pattern.toLowerCase();
|
|
293
|
+
}
|
|
294
|
+
return pattern === path;
|
|
295
|
+
}
|
|
296
|
+
if (pattern === EMPTY_REGEX) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return pattern.test(path);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Registers a route, which every method helper and use() funnel into. Several paths at once
|
|
304
|
+
* become several routes sharing the callbacks, as Express allows. Paths are normalised here and
|
|
305
|
+
* not at match time: no trailing slash unless strict routing, "*" becomes "/{*splat}", and
|
|
306
|
+
* anything not comparable as a string is compiled to a regular expression and marked complex.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} method HTTP method, or USE for a mount
|
|
309
|
+
* @param {any} path one path or several
|
|
310
|
+
* @param {any} [parent] what to return, so chaining lands on the app rather than the router
|
|
311
|
+
* @param {...any} callbacks
|
|
312
|
+
* @returns {any} parent
|
|
313
|
+
*/
|
|
314
|
+
createRoute(method, path, parent = this, ...callbacks) {
|
|
315
|
+
method = method.toUpperCase();
|
|
316
|
+
callbacks = callbacks.flat();
|
|
317
|
+
const paths = Array.isArray(path) ? path : [path];
|
|
318
|
+
const routes = [];
|
|
319
|
+
for (let path of paths) {
|
|
320
|
+
if (!this.get("strict routing") && typeof path === "string" && path.endsWith("/") && path !== "/") {
|
|
321
|
+
path = path.slice(0, -1);
|
|
322
|
+
}
|
|
323
|
+
if (path === "*") {
|
|
324
|
+
path = "/{*splat}";
|
|
325
|
+
}
|
|
326
|
+
const route = {
|
|
327
|
+
method: method === "USE" ? "ALL" : method,
|
|
328
|
+
path,
|
|
329
|
+
pattern:
|
|
330
|
+
method === "USE" || needsConversionToRegex(path) ? patternToRegex(path, method === "USE") : path,
|
|
331
|
+
callbacks,
|
|
332
|
+
routeKey: routeKey++,
|
|
333
|
+
// the router this was registered on. Ordinary dispatch is done by that router, so
|
|
334
|
+
// it could ask itself, but an optimized chain is walked by the app whatever it
|
|
335
|
+
// contains, and param() callbacks belong to the router that declared them
|
|
336
|
+
owner: this,
|
|
337
|
+
// and its callbacks by reference, since dispatch asks for them on every hop of
|
|
338
|
+
// every request and param() only ever writes into this map, never replaces it.
|
|
339
|
+
// Reading them through owner measured 8 microseconds per thousand requests
|
|
340
|
+
paramCallbacks: this._paramCallbacks,
|
|
341
|
+
use: method === "USE",
|
|
342
|
+
all: method === "ALL" || method === "USE",
|
|
343
|
+
gettable: method === "GET" || method === "HEAD"
|
|
344
|
+
};
|
|
345
|
+
if (
|
|
346
|
+
typeof route.path === "string" &&
|
|
347
|
+
(route.path.includes(":") || route.path.includes("*") || route.path.includes("{")) &&
|
|
348
|
+
route.pattern instanceof RegExp
|
|
349
|
+
) {
|
|
350
|
+
route.complex = true;
|
|
351
|
+
}
|
|
352
|
+
routes.push(route);
|
|
353
|
+
}
|
|
354
|
+
this._routes.push(...routes);
|
|
355
|
+
|
|
356
|
+
return parent;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* The chain a request would walk to reach this route, or false when it cannot be known ahead of
|
|
361
|
+
* time. The native router jumps straight to the route, so everything registered before it that
|
|
362
|
+
* could also match has to be in the chain, in order.
|
|
363
|
+
*
|
|
364
|
+
* @param {any} route
|
|
365
|
+
* @param {any[]} routes every route of this router, in registration order
|
|
366
|
+
* @returns {any[]|false} the chain, ending in the route itself
|
|
367
|
+
*/
|
|
368
|
+
_optimizeRoute(route, routes) {
|
|
369
|
+
const optimizedPath = [];
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < routes.length; i++) {
|
|
372
|
+
const r = routes[i];
|
|
373
|
+
if (r.routeKey > route.routeKey) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (r === route) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// if the methods are not the same, and its not an all method, skip it
|
|
380
|
+
if (!r.all && r.method !== route.method) {
|
|
381
|
+
// check if the methods are compatible (GET and HEAD)
|
|
382
|
+
if (!(r.method === "HEAD" && route.method === "GET")) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// check if the paths match
|
|
388
|
+
if (
|
|
389
|
+
(r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
|
|
390
|
+
(typeof r.pattern === "string" && (r.pattern === route.path || r.pattern === "/*"))
|
|
391
|
+
) {
|
|
392
|
+
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
393
|
+
return false; // cant optimize nested routers with matches
|
|
394
|
+
}
|
|
395
|
+
optimizedPath.push(r);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
optimizedPath.push(route);
|
|
399
|
+
|
|
400
|
+
return optimizedPath;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Hands every route reachable by path alone to the native uWS router, walking into mounted
|
|
405
|
+
* routers and carrying their prefix down. Runs once, when the app starts listening, since it
|
|
406
|
+
* needs every route to have been registered first.
|
|
407
|
+
*/
|
|
408
|
+
_compileOptimizedRoutes() {
|
|
409
|
+
if (!this.uwsApp || !this.get("case sensitive routing")) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts
|
|
414
|
+
const walk = (router, pathPrefix, chainPrefix) => {
|
|
415
|
+
for (const route of router._routes) {
|
|
416
|
+
if (route.use) {
|
|
417
|
+
// only sole-callback mounts
|
|
418
|
+
if (
|
|
419
|
+
!route.complex &&
|
|
420
|
+
canBeOptimized(route.path) &&
|
|
421
|
+
route.path !== "/*" &&
|
|
422
|
+
route.callbacks.length === 1 &&
|
|
423
|
+
route.callbacks[0] instanceof Router
|
|
424
|
+
) {
|
|
425
|
+
let pathToMount = router._optimizeRoute(route, router._routes);
|
|
426
|
+
if (!pathToMount) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
pathToMount = pathToMount.slice(0, -1);
|
|
430
|
+
walk(route.callbacks[0], pathPrefix + route.path, [
|
|
431
|
+
...chainPrefix,
|
|
432
|
+
...pathToMount,
|
|
433
|
+
{
|
|
434
|
+
...route,
|
|
435
|
+
callbacks: [],
|
|
436
|
+
keepMount: true,
|
|
437
|
+
// mounted sub-apps become req.app during their dispatch, like express
|
|
438
|
+
mountApp:
|
|
439
|
+
route.callbacks[0].constructor.name === "Application"
|
|
440
|
+
? route.callbacks[0]
|
|
441
|
+
: undefined
|
|
442
|
+
}
|
|
443
|
+
]);
|
|
444
|
+
}
|
|
445
|
+
// µWS picks by specificity and Express by registration order, so the chain
|
|
446
|
+
// computed for whichever route µWS lands on runs everything that could have
|
|
447
|
+
// matched before it
|
|
448
|
+
} else if (
|
|
449
|
+
(canBeOptimized(route.path) ||
|
|
450
|
+
// parameters that are whole segments are matched by µWS the same way
|
|
451
|
+
(canBeOptimizedWithParams(route.path) &&
|
|
452
|
+
// inside a mounted router, only when nothing after it could match
|
|
453
|
+
(!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)))) &&
|
|
454
|
+
supportedUwsMethods.has(route.method)
|
|
455
|
+
) {
|
|
456
|
+
const leafPath = router._optimizeRoute(route, router._routes);
|
|
457
|
+
if (!leafPath) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
// param route earlier in the same router would steal this static path
|
|
461
|
+
if (leafPath.length > 1) {
|
|
462
|
+
const shadow = leafPath[leafPath.length - 2];
|
|
463
|
+
if (
|
|
464
|
+
shadow &&
|
|
465
|
+
!shadow.use &&
|
|
466
|
+
shadow.method === route.method &&
|
|
467
|
+
shadow.path !== route.path &&
|
|
468
|
+
shadow.pattern instanceof RegExp
|
|
469
|
+
) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (pathPrefix) {
|
|
474
|
+
this._registerUwsRoute(
|
|
475
|
+
{
|
|
476
|
+
...route,
|
|
477
|
+
path: pathPrefix + route.path,
|
|
478
|
+
pattern: pathPrefix + route.path,
|
|
479
|
+
optimizedRouter: true
|
|
480
|
+
},
|
|
481
|
+
[...chainPrefix, ...leafPath]
|
|
482
|
+
);
|
|
483
|
+
} else {
|
|
484
|
+
this._registerUwsRoute(route, leafPath);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
walk(this, "", []);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Wraps a uWS request and response in ours and links them, which is the first thing every
|
|
495
|
+
* request does whichever path serves it.
|
|
496
|
+
*
|
|
497
|
+
* @param {any} res uWS response
|
|
498
|
+
* @param {any} req uWS request, readable only during this call
|
|
499
|
+
* @returns {{request: any, response: any}}
|
|
500
|
+
*/
|
|
501
|
+
handleRequest(res, req) {
|
|
502
|
+
const request = new this._request(req, res, this);
|
|
503
|
+
const response = new this._response(res, request, this);
|
|
504
|
+
request.res = response;
|
|
505
|
+
response.req = request;
|
|
506
|
+
res.onAborted(() => {
|
|
507
|
+
/** @type {NodeJS.ErrnoException} */
|
|
508
|
+
const err = new Error("Connection closed");
|
|
509
|
+
err.code = "ECONNRESET";
|
|
510
|
+
response.aborted = true;
|
|
511
|
+
response.finished = true;
|
|
512
|
+
response.socket?.emit("error", err);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return { request, response };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Whether a route registered later in the same router could match a path this one matches.
|
|
520
|
+
*
|
|
521
|
+
* A route inside a mounted router may only go to µWS when the answer is no: a native chain that
|
|
522
|
+
* runs out resumes after the mount, not inside the router, so a later sibling would be lost. A
|
|
523
|
+
* mount or a pattern of an unknown shape counts as an overlap; two paths µWS could match itself
|
|
524
|
+
* are compared segment by segment.
|
|
525
|
+
*
|
|
526
|
+
* @param {any} route
|
|
527
|
+
* @param {any[]} routes every route of the router this one belongs to
|
|
528
|
+
* @returns {boolean}
|
|
529
|
+
*/
|
|
530
|
+
_isFollowedByAnOverlap(route, routes) {
|
|
531
|
+
for (let i = routes.length - 1; i >= 0; i--) {
|
|
532
|
+
const later = routes[i];
|
|
533
|
+
if (later.routeKey <= route.routeKey) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
// a different verb cannot answer the same request, unless it answers every verb
|
|
537
|
+
if (!later.all && !later.use && later.method !== route.method) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (later.use) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
if (typeof later.path === "string" && canBeOptimizedWithParams(later.path)) {
|
|
544
|
+
if (pathsCanOverlap(route.path, later.path)) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Hands one route to µWS, along with the chain of everything that has to run in front of it,
|
|
556
|
+
* and records that chain on the route so the handler can walk it.
|
|
557
|
+
*
|
|
558
|
+
* @param {any} route
|
|
559
|
+
* @param {any[]} optimizedPath the routes to run, in order, ending with this one
|
|
560
|
+
*/
|
|
561
|
+
_registerUwsRoute(route, optimizedPath) {
|
|
562
|
+
let method = route.method.toLowerCase();
|
|
563
|
+
if (method === "all") {
|
|
564
|
+
method = "any";
|
|
565
|
+
} else if (method === "delete") {
|
|
566
|
+
method = "del";
|
|
567
|
+
}
|
|
568
|
+
if (route.path.includes(":")) {
|
|
569
|
+
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
570
|
+
}
|
|
571
|
+
let fn = async (res, req) => {
|
|
572
|
+
const { request, response } = this.handleRequest(res, req);
|
|
573
|
+
if (route.optimizedParams) {
|
|
574
|
+
request.optimizedParams = new NullObject();
|
|
575
|
+
for (let i = 0; i < route.optimizedParams.length; i++) {
|
|
576
|
+
request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// falling back resumes after the mount, not after the router's leaf: the leaf can have a
|
|
580
|
+
// lower routeKey than the parent's middlewares, and an error handler declared before the
|
|
581
|
+
// mount must not catch what the router threw
|
|
582
|
+
const mount = optimizedPath.find((r) => r.keepMount);
|
|
583
|
+
const skipUntil = mount ?? (optimizedPath.length ? optimizedPath[optimizedPath.length - 1] : route);
|
|
584
|
+
const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, skipUntil);
|
|
585
|
+
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
586
|
+
this._endUnmatched(request, response);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
route.optimizedPath = optimizedPath;
|
|
590
|
+
|
|
591
|
+
let replacedPath = route.path;
|
|
592
|
+
const realFn = fn;
|
|
593
|
+
|
|
594
|
+
// check if route is declarative
|
|
595
|
+
if (
|
|
596
|
+
optimizedPath.length === 1 && // must not have middlewares
|
|
597
|
+
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
598
|
+
typeof route.callbacks[0] === "function" && // must be a function
|
|
599
|
+
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
600
|
+
!resDecMethods.some((method) => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
|
|
601
|
+
this.get("declarative responses") // must have declarative responses enabled
|
|
602
|
+
) {
|
|
603
|
+
const decRes = compileDeclarative(route.callbacks[0], this);
|
|
604
|
+
if (decRes) {
|
|
605
|
+
fn = decRes;
|
|
606
|
+
}
|
|
607
|
+
} else {
|
|
608
|
+
replacedPath = route.path.replace(regExParam, ":x");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
this.uwsApp[method](replacedPath, fn);
|
|
612
|
+
// the route's own router decides, not the app running the registration: a router created
|
|
613
|
+
// with { strict: true } and mounted on an app without it does not answer /things/, and
|
|
614
|
+
// registering that path here is the only way it could
|
|
615
|
+
if (!(route.owner ?? this).get("strict routing") && route.path[route.path.length - 1] !== "/") {
|
|
616
|
+
this.uwsApp[method](replacedPath + "/", fn);
|
|
617
|
+
if (method === "get") {
|
|
618
|
+
this.uwsApp.head(replacedPath + "/", realFn);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (method === "get") {
|
|
622
|
+
this.uwsApp.head(replacedPath, realFn);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Gives an error to the handler that asked for it, or answers with it when there is none.
|
|
628
|
+
* Passing something to next() from an error handler clears the error and resumes routing,
|
|
629
|
+
* which is how Express lets a handler decide the error was not fatal.
|
|
630
|
+
*
|
|
631
|
+
* @param {any} err
|
|
632
|
+
* @param {Function|null} handler the four-argument handler to call, or null for the default
|
|
633
|
+
* @param {any} request
|
|
634
|
+
* @param {any} response
|
|
635
|
+
*/
|
|
636
|
+
_handleError(err, handler, request, response) {
|
|
637
|
+
if (handler) {
|
|
638
|
+
return handler(err, request, response, (pass) => {
|
|
639
|
+
delete request._error;
|
|
640
|
+
delete request._errorKey;
|
|
641
|
+
return request.next(pass);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
console.error(err);
|
|
645
|
+
if (response.statusCode === 200) {
|
|
646
|
+
response.statusCode = 500;
|
|
647
|
+
}
|
|
648
|
+
this._sendErrorPage(request, response, err, true);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* The HTML for an error, which in production says only what the status means rather than what
|
|
653
|
+
* went wrong, so a stack trace does not reach the client.
|
|
654
|
+
*
|
|
655
|
+
* @param {any} err
|
|
656
|
+
* @param {number} statusCode
|
|
657
|
+
* @param {boolean} [checkEnv] whether production should redact it
|
|
658
|
+
* @returns {string}
|
|
659
|
+
*/
|
|
660
|
+
_generateErrorPage(err, statusCode, checkEnv = false) {
|
|
661
|
+
if (checkEnv && this.get("env") === "production") {
|
|
662
|
+
err =
|
|
663
|
+
statusCode >= 400 ? (statuses.message[statusCode] ?? "Internal Server Error") : "Internal Server Error";
|
|
664
|
+
}
|
|
665
|
+
return generateErrorPageHtml(err);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* @param {import("./utils.js").PathRegExp} pattern
|
|
670
|
+
* @param {string} path
|
|
671
|
+
*/
|
|
672
|
+
_extractParams(pattern, path) {
|
|
673
|
+
let match = pattern.exec(path);
|
|
674
|
+
if (!match && path.length > 1 && path.endsWith("/")) {
|
|
675
|
+
// a pattern compiled without a trailing slash still matches a path written with one,
|
|
676
|
+
// which is what non-strict routing means. Retried rather than stripped up front, so
|
|
677
|
+
// that a wildcard captures the path as it arrived and "/a/b/" keeps its last, empty
|
|
678
|
+
// segment the way Express reports it
|
|
679
|
+
match = pattern.exec(path.slice(0, -1));
|
|
680
|
+
}
|
|
681
|
+
// Object.create(null) rather than the { __proto__: null } literal, which is the same object
|
|
682
|
+
// for 9ns more. Null-prototyped either way, as Express 5 makes params.
|
|
683
|
+
const obj = Object.create(null);
|
|
684
|
+
if (!match?.groups) {
|
|
685
|
+
return obj;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const groups = match.groups;
|
|
689
|
+
const meta = getPatternMeta(pattern);
|
|
690
|
+
if (meta === undefined) {
|
|
691
|
+
// a RegExp the application supplied itself, which was never compiled here
|
|
692
|
+
for (const name in groups) {
|
|
693
|
+
const value = groups[name];
|
|
694
|
+
if (value === undefined) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
obj[name] = decodeParam(value);
|
|
698
|
+
}
|
|
699
|
+
return obj;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// asking for each name in turn rather than walking the groups object, which is a
|
|
703
|
+
// null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
|
|
704
|
+
// worked out when the pattern was compiled instead of searching an array for it
|
|
705
|
+
const { paramNames, isWildcard } = meta;
|
|
706
|
+
for (let i = 0, len = paramNames.length; i < len; i++) {
|
|
707
|
+
const name = paramNames[i];
|
|
708
|
+
const value = groups[name];
|
|
709
|
+
// an optional group that did not match is absent in v5, not present as undefined
|
|
710
|
+
if (value === undefined) {
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
// a wildcard is an array of segments in v5, and each segment is decoded on its own so
|
|
714
|
+
// that an encoded slash inside one stays inside it
|
|
715
|
+
obj[name] = isWildcard[i] ? value.split("/").map(decodeParam) : decodeParam(value);
|
|
716
|
+
}
|
|
717
|
+
return obj;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Fills in what a route needs before its handlers run: req.route, req.params from the pattern
|
|
722
|
+
* and from any mergeParams parents, and the app.param callbacks for the parameters this route
|
|
723
|
+
* matched that this request has not already seen.
|
|
724
|
+
*
|
|
725
|
+
* @param {any} req
|
|
726
|
+
* @param {any} res
|
|
727
|
+
* @param {any} route
|
|
728
|
+
* @returns {any} a promise only when a param callback is involved
|
|
729
|
+
*/
|
|
730
|
+
_preprocessRequest(req, res, route) {
|
|
731
|
+
req.route = route;
|
|
732
|
+
// both, not the route flag alone: the flag says the route was registered natively, the
|
|
733
|
+
// values say this request came in that way
|
|
734
|
+
if (route.optimizedParams && req.optimizedParams) {
|
|
735
|
+
req.params = Object.create(null);
|
|
736
|
+
try {
|
|
737
|
+
// µWS hands back the raw text, as the regex does, so both paths decode here
|
|
738
|
+
for (const name in req.optimizedParams) {
|
|
739
|
+
req.params[name] = decodeParam(req.optimizedParams[name]);
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
req._error = err;
|
|
743
|
+
req._errorKey = route.routeKey;
|
|
744
|
+
return "route";
|
|
745
|
+
}
|
|
746
|
+
} else if (route.complex) {
|
|
747
|
+
let path = req._originalPath;
|
|
748
|
+
if (req._stack.length > 0) {
|
|
749
|
+
const fullMountpath = this.getFullMountpath(req);
|
|
750
|
+
if (fullMountpath !== EMPTY_REGEX) {
|
|
751
|
+
path = path.replace(fullMountpath, "");
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
req.params = this._extractParams(route.pattern, path);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
// a parameter that will not decode. Express throws out of the match and lets the
|
|
758
|
+
// error reach the error handler, which answers 400, so the route is skipped rather
|
|
759
|
+
// than run with a value nobody can read.
|
|
760
|
+
req._error = err;
|
|
761
|
+
req._errorKey = route.routeKey;
|
|
762
|
+
return "route";
|
|
763
|
+
}
|
|
764
|
+
if (req._paramStack.length > 0) {
|
|
765
|
+
for (const params of req._paramStack) {
|
|
766
|
+
req.params = Object.assign(Object.create(null), params, req.params);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
req.params = {};
|
|
771
|
+
if (req._paramStack.length > 0) {
|
|
772
|
+
for (const params of req._paramStack) {
|
|
773
|
+
req.params = Object.assign(Object.create(null), params, req.params);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// the route's own router's callbacks: an optimized chain is walked by the app even when it
|
|
779
|
+
// ends in a mounted router's route
|
|
780
|
+
const paramCallbacks = route.paramCallbacks;
|
|
781
|
+
if (paramCallbacks.size > 0) {
|
|
782
|
+
// known issue: an async executor swallows what it throws, so a param callback that
|
|
783
|
+
// throws synchronously is lost. Fixing it moves when the callbacks run
|
|
784
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
785
|
+
return new Promise(async (resolve) => {
|
|
786
|
+
for (const param in req.params) {
|
|
787
|
+
const pcs = paramCallbacks.get(param);
|
|
788
|
+
// built here rather than for every request, since only an application using
|
|
789
|
+
// app.param() ever reaches this line
|
|
790
|
+
if (pcs && !req._gotParams?.has(param)) {
|
|
791
|
+
(req._gotParams ??= new Set()).add(param);
|
|
792
|
+
for (let i = 0, len = pcs.length; i < len; i++) {
|
|
793
|
+
const fn = pcs[i];
|
|
794
|
+
await /** @type {Promise<void>} */ (
|
|
795
|
+
new Promise((resolveRoute) => {
|
|
796
|
+
const next = (thingamabob) => {
|
|
797
|
+
if (thingamabob) {
|
|
798
|
+
if (thingamabob === "route") {
|
|
799
|
+
return resolve("route");
|
|
800
|
+
} else {
|
|
801
|
+
req._error = thingamabob;
|
|
802
|
+
req._errorKey = route.routeKey;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return resolveRoute();
|
|
806
|
+
};
|
|
807
|
+
req.next = next;
|
|
808
|
+
fn(req, res, next, req.params[param], param);
|
|
809
|
+
})
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
resolve(true);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Registers a callback that runs whenever a route parameter of this name is matched, before
|
|
823
|
+
* the route's own handlers, once per request per parameter.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* app.param("id", (req, res, next, value) => { req.user = lookup(value); next(); });
|
|
827
|
+
*
|
|
828
|
+
* @param {string|string[]} name parameter name, or several
|
|
829
|
+
* @param {(req: object, res: object, next: Function, value: string, name: string) => void} fn
|
|
830
|
+
* @returns {this} the router, for chaining
|
|
831
|
+
* @throws {TypeError} if name is neither a string nor an array
|
|
832
|
+
*/
|
|
833
|
+
param(name, fn) {
|
|
834
|
+
// the message has to read exactly like this: it is the one the router package throws,
|
|
835
|
+
// and it is what reaches anyone catching it
|
|
836
|
+
if (typeof name !== "string" && !Array.isArray(name)) {
|
|
837
|
+
throw new TypeError("argument name must be a string");
|
|
838
|
+
}
|
|
839
|
+
const names = Array.isArray(name) ? name : [name];
|
|
840
|
+
for (const key of names) {
|
|
841
|
+
if (!this._paramCallbacks.has(key)) {
|
|
842
|
+
this._paramCallbacks.set(key, []);
|
|
843
|
+
}
|
|
844
|
+
this._paramCallbacks.get(key).push(fn);
|
|
845
|
+
}
|
|
846
|
+
return this;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Resolves with the route that answered, or false when nothing matched.
|
|
851
|
+
* @returns {Promise<any>}
|
|
852
|
+
*/
|
|
853
|
+
_routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
854
|
+
return new Promise((resolve, reject) => {
|
|
855
|
+
this._dispatchRoute(req, res, startIndex, routes, skipCheck, skipUntil, resolve, reject);
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Finds the next route that matches and runs it, carrying the same resolve and reject the whole
|
|
861
|
+
* way. next() calls this again for the route after, so a chain of N middlewares costs one
|
|
862
|
+
* promise instead of N nested ones.
|
|
863
|
+
*
|
|
864
|
+
* @param {any} req
|
|
865
|
+
* @param {any} res
|
|
866
|
+
* @param {number} startIndex where to resume the scan
|
|
867
|
+
* @param {any[]} routes
|
|
868
|
+
* @param {boolean} skipCheck take the route at startIndex without matching it, which is how an
|
|
869
|
+
* already-decided chain is walked
|
|
870
|
+
* @param {any} skipUntil route to resume after when this chain runs out, or undefined
|
|
871
|
+
* @param {(value: any) => void} resolve
|
|
872
|
+
* @param {(err: any) => void} reject
|
|
873
|
+
*/
|
|
874
|
+
_dispatchRoute(req, res, startIndex, routes, skipCheck, skipUntil, resolve, reject) {
|
|
875
|
+
const routeIndex = skipCheck
|
|
876
|
+
? startIndex
|
|
877
|
+
: findIndexStartingFrom(
|
|
878
|
+
routes,
|
|
879
|
+
(r) =>
|
|
880
|
+
(r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) &&
|
|
881
|
+
this._pathMatches(r, req),
|
|
882
|
+
startIndex
|
|
883
|
+
);
|
|
884
|
+
const route = routes[routeIndex];
|
|
885
|
+
if (!route) {
|
|
886
|
+
if (!skipCheck) {
|
|
887
|
+
// on normal unoptimized routes, if theres no match then there is no route
|
|
888
|
+
return resolve(false);
|
|
889
|
+
}
|
|
890
|
+
// the chain ran out, so ordinary routing takes over from the top and skips what has
|
|
891
|
+
// already run
|
|
892
|
+
useApp(req, this);
|
|
893
|
+
// a chain that went into a mount never left it, since keepMount stops the pop, so the
|
|
894
|
+
// path is still relative to it. /alone/skip must not be offered to the app as /skip
|
|
895
|
+
if (req._stack.length > 0) {
|
|
896
|
+
req._stack.length = 0;
|
|
897
|
+
req._stackMounted = 0;
|
|
898
|
+
req.path = req._originalPath;
|
|
899
|
+
req.url = req._originalPath + req.urlQuery;
|
|
900
|
+
req._opPath =
|
|
901
|
+
req.endsWithSlash && req._originalPath !== "/" && !this.get("strict routing")
|
|
902
|
+
? req._originalPath.slice(0, -1)
|
|
903
|
+
: req._originalPath;
|
|
904
|
+
}
|
|
905
|
+
// an error out of a mount is attributed to the mount, so error handlers declared before
|
|
906
|
+
// it do not catch it, as in ordinary dispatch
|
|
907
|
+
if (req._error && skipUntil && skipUntil.keepMount && skipUntil.routeKey > req._errorKey) {
|
|
908
|
+
req._errorKey = skipUntil.routeKey;
|
|
909
|
+
}
|
|
910
|
+
return this._dispatchRoute(req, res, 0, this._routes, false, skipUntil, resolve, reject);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// _preprocessRequest returns a promise only when there are param callbacks, so the common
|
|
914
|
+
// case stays synchronous. A microtask every 300 routes resets the stack, which a long chain
|
|
915
|
+
// would otherwise blow
|
|
916
|
+
const continueRoute = this._preprocessRequest(req, res, route);
|
|
917
|
+
if (route.paramCallbacks.size !== 0 || req.routeCount % 300 === 0) {
|
|
918
|
+
Promise.resolve(continueRoute).then(
|
|
919
|
+
(resumed) =>
|
|
920
|
+
this._runRoute(req, res, routeIndex, route, routes, skipCheck, skipUntil, resolve, reject, resumed),
|
|
921
|
+
reject
|
|
922
|
+
);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
return this._runRoute(
|
|
926
|
+
req,
|
|
927
|
+
res,
|
|
928
|
+
routeIndex,
|
|
929
|
+
route,
|
|
930
|
+
routes,
|
|
931
|
+
skipCheck,
|
|
932
|
+
skipUntil,
|
|
933
|
+
resolve,
|
|
934
|
+
reject,
|
|
935
|
+
continueRoute
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Runs one route's callbacks, one after another through next(). A mount adjusts req.url,
|
|
941
|
+
* req.path and the mount stack on the way in, and puts them back if next("route") leaves it.
|
|
942
|
+
*
|
|
943
|
+
* @param {any} req
|
|
944
|
+
* @param {any} res
|
|
945
|
+
* @param {number} routeIndex
|
|
946
|
+
* @param {any} route
|
|
947
|
+
* @param {any[]} routes
|
|
948
|
+
* @param {boolean} skipCheck
|
|
949
|
+
* @param {any} skipUntil
|
|
950
|
+
* @param {(value: any) => void} resolve
|
|
951
|
+
* @param {(err: any) => void} reject
|
|
952
|
+
* @param {any} continueRoute
|
|
953
|
+
*/
|
|
954
|
+
_runRoute(req, res, routeIndex, route, routes, skipCheck, skipUntil, resolve, reject, continueRoute) {
|
|
955
|
+
let callbackindex = 0;
|
|
956
|
+
if (route.use) {
|
|
957
|
+
if (route.mountApp) {
|
|
958
|
+
// optimized chain: normal dispatch swaps req.app when it enters a mounted Application,
|
|
959
|
+
// but the compiled mount route has no callback to do it, so swap it here
|
|
960
|
+
useApp(req, route.mountApp);
|
|
961
|
+
}
|
|
962
|
+
req._stack.push(route.path);
|
|
963
|
+
// a use with no path consumes nothing, so everything below would work out the values
|
|
964
|
+
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
965
|
+
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
966
|
+
if (route.path !== "" || req.endsWithSlash) {
|
|
967
|
+
if (route.path !== "") {
|
|
968
|
+
req._stackMounted++;
|
|
969
|
+
}
|
|
970
|
+
const fullMountpath = this.getFullMountpath(req);
|
|
971
|
+
req._opPath =
|
|
972
|
+
fullMountpath !== EMPTY_REGEX ? req._originalPath.replace(fullMountpath, "") : req._originalPath;
|
|
973
|
+
if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
|
|
974
|
+
req._opPath = this.get("strict routing") ? req._opPath + "/" : req._opPath.slice(0, -1);
|
|
975
|
+
}
|
|
976
|
+
req.url = req._opPath + req.urlQuery;
|
|
977
|
+
req.path = req._opPath;
|
|
978
|
+
if (req._opPath === "") {
|
|
979
|
+
req.url = "/";
|
|
980
|
+
req.path = "/";
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
// plain (non-async) function: an async next() would allocate an unconsumed promise
|
|
985
|
+
// on every middleware/handler step of every request
|
|
986
|
+
const next = (thingamabob) => {
|
|
987
|
+
if (thingamabob) {
|
|
988
|
+
if (thingamabob === "route") {
|
|
989
|
+
if (route.use && !route.keepMount) {
|
|
990
|
+
if (req._stack.pop() !== "") {
|
|
991
|
+
req._stackMounted--;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const strictRouting = this.get("strict routing");
|
|
995
|
+
const poppedMountpath = req._stack.length > 0 ? this.getFullMountpath(req) : EMPTY_REGEX;
|
|
996
|
+
req._opPath =
|
|
997
|
+
poppedMountpath !== EMPTY_REGEX
|
|
998
|
+
? req._originalPath.replace(poppedMountpath, "")
|
|
999
|
+
: req._originalPath;
|
|
1000
|
+
if (strictRouting) {
|
|
1001
|
+
if (req.endsWithSlash && req._opPath[req._opPath.length - 1] !== "/") {
|
|
1002
|
+
req._opPath += "/";
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
req.url = req._opPath + req.urlQuery;
|
|
1006
|
+
req.path = req._opPath;
|
|
1007
|
+
if (req._opPath === "") {
|
|
1008
|
+
req.url = "/";
|
|
1009
|
+
req.path = "/";
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
!strictRouting &&
|
|
1013
|
+
req.endsWithSlash &&
|
|
1014
|
+
req._originalPath !== "/" &&
|
|
1015
|
+
req._opPath[req._opPath.length - 1] === "/"
|
|
1016
|
+
) {
|
|
1017
|
+
req._opPath = req._opPath.slice(0, -1);
|
|
1018
|
+
}
|
|
1019
|
+
if (req.app.parent && route.callbacks[0]?.constructor.name === "Application") {
|
|
1020
|
+
useApp(req, req.app.parent);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
req.routeCount++;
|
|
1024
|
+
// _dispatchRoute is a plain function, so a synchronous throw would escape here instead
|
|
1025
|
+
// of rejecting, like it used to when this recursed through the async _routeRequest
|
|
1026
|
+
try {
|
|
1027
|
+
return this._dispatchRoute(
|
|
1028
|
+
req,
|
|
1029
|
+
res,
|
|
1030
|
+
routeIndex + 1,
|
|
1031
|
+
routes,
|
|
1032
|
+
skipCheck,
|
|
1033
|
+
skipUntil,
|
|
1034
|
+
resolve,
|
|
1035
|
+
reject
|
|
1036
|
+
);
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
return reject(err);
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
req._error = thingamabob;
|
|
1042
|
+
req._errorKey = route.routeKey;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const callback = route.callbacks[callbackindex++];
|
|
1046
|
+
if (!callback) {
|
|
1047
|
+
return next("route");
|
|
1048
|
+
}
|
|
1049
|
+
// skipping routes we already went through via optimized path. Before the Router branch
|
|
1050
|
+
// below and not after it: a mount whose chain was compiled has already run, and running
|
|
1051
|
+
// it again would answer from inside the router a request that had just left it
|
|
1052
|
+
if (!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
|
|
1053
|
+
return next();
|
|
1054
|
+
}
|
|
1055
|
+
if (callback instanceof Router) {
|
|
1056
|
+
if (callback.constructor.name === "Application") {
|
|
1057
|
+
useApp(req, callback);
|
|
1058
|
+
}
|
|
1059
|
+
if (callback.settings.mergeParams) {
|
|
1060
|
+
req._paramStack.push(req.params);
|
|
1061
|
+
}
|
|
1062
|
+
if (
|
|
1063
|
+
callback.settings["strict routing"] &&
|
|
1064
|
+
req.endsWithSlash &&
|
|
1065
|
+
req._opPath[req._opPath.length - 1] !== "/"
|
|
1066
|
+
) {
|
|
1067
|
+
req._opPath += "/";
|
|
1068
|
+
}
|
|
1069
|
+
callback._routeRequest(req, res, 0).then((routed) => {
|
|
1070
|
+
if (req._error) {
|
|
1071
|
+
req._errorKey = route.routeKey;
|
|
1072
|
+
}
|
|
1073
|
+
if (routed) return resolve(true);
|
|
1074
|
+
if (req._isOptions && req._matchedMethods.size) {
|
|
1075
|
+
// OPTIONS routing is different, it stops in the router if matched
|
|
1076
|
+
return resolve(false);
|
|
1077
|
+
}
|
|
1078
|
+
next();
|
|
1079
|
+
});
|
|
1080
|
+
} else {
|
|
1081
|
+
// handle errors and error handlers
|
|
1082
|
+
if (req._error || callback.length === 4) {
|
|
1083
|
+
if (req._error && callback.length === 4 && route.routeKey >= req._errorKey) {
|
|
1084
|
+
return this._handleError(req._error, callback, req, res);
|
|
1085
|
+
} else {
|
|
1086
|
+
return next();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
// handling OPTIONS method
|
|
1092
|
+
if (req._isOptions && !route.all && route.method !== "OPTIONS") {
|
|
1093
|
+
req._matchedMethods.add(route.method);
|
|
1094
|
+
if (route.gettable) {
|
|
1095
|
+
req._matchedMethods.add("HEAD");
|
|
1096
|
+
}
|
|
1097
|
+
return next();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const out = callback(req, res, next);
|
|
1101
|
+
if (out instanceof Promise) {
|
|
1102
|
+
// Express 5 forwards a rejected handler promise to the error middleware on
|
|
1103
|
+
// its own, so there is nothing left for the "catch async errors" setting or
|
|
1104
|
+
// for express-async-errors to opt into
|
|
1105
|
+
out.catch((err) => {
|
|
1106
|
+
req._error = err;
|
|
1107
|
+
req._errorKey = route.routeKey;
|
|
1108
|
+
return next();
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
req._error = err;
|
|
1113
|
+
req._errorKey = route.routeKey;
|
|
1114
|
+
return next();
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
req.next = next;
|
|
1119
|
+
if (continueRoute === "route") {
|
|
1120
|
+
next("route");
|
|
1121
|
+
} else if (continueRoute) {
|
|
1122
|
+
next();
|
|
1123
|
+
} else {
|
|
1124
|
+
resolve(true);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Mounts middleware, or a whole router, at a path. The path is optional, and a mount matches
|
|
1130
|
+
* everything under it, which is what separates it from all(). Mounting a Router sets its
|
|
1131
|
+
* mountpath and parent and emits 'mount' on it.
|
|
1132
|
+
*
|
|
1133
|
+
* @param {string|string[]|Function|Router|Array<Function|Router>} [path] mount path, or the
|
|
1134
|
+
* first handler
|
|
1135
|
+
* @param {...(Function|Router|Array<Function|Router>)} callbacks handlers, nested arrays allowed
|
|
1136
|
+
* @returns {this} the router, for chaining
|
|
1137
|
+
*/
|
|
1138
|
+
use(path, ...callbacks) {
|
|
1139
|
+
if (
|
|
1140
|
+
typeof path === "function" ||
|
|
1141
|
+
path instanceof Router ||
|
|
1142
|
+
(Array.isArray(path) && path.every((p) => typeof p === "function" || p instanceof Router))
|
|
1143
|
+
) {
|
|
1144
|
+
callbacks.unshift(path);
|
|
1145
|
+
path = "";
|
|
1146
|
+
}
|
|
1147
|
+
if (path === "/") {
|
|
1148
|
+
path = "";
|
|
1149
|
+
}
|
|
1150
|
+
callbacks = callbacks.flat();
|
|
1151
|
+
|
|
1152
|
+
for (const callback of callbacks) {
|
|
1153
|
+
if (callback instanceof Router) {
|
|
1154
|
+
callback.mountpath = /** @type {string|string[]} */ (path);
|
|
1155
|
+
callback.parent = this;
|
|
1156
|
+
callback.emit("mount", this);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
this.createRoute("USE", path, this, ...callbacks);
|
|
1160
|
+
return this;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* A builder for one path, so the path is written once and the verbs chain off it.
|
|
1165
|
+
*
|
|
1166
|
+
* @example
|
|
1167
|
+
* app.route("/book").get(list).post(create);
|
|
1168
|
+
*
|
|
1169
|
+
* @param {string} path the path every verb on the returned object registers against
|
|
1170
|
+
* @returns {object} an object with one method per HTTP verb, each returning it again
|
|
1171
|
+
*/
|
|
1172
|
+
route(path) {
|
|
1173
|
+
const fns = new NullObject();
|
|
1174
|
+
for (const method of methods) {
|
|
1175
|
+
fns[method] = (...callbacks) => {
|
|
1176
|
+
return this.createRoute(method, path, /** @type {any} */ (fns), ...callbacks);
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
fns.get = (...callbacks) => {
|
|
1180
|
+
return this.createRoute("GET", path, /** @type {any} */ (fns), ...callbacks);
|
|
1181
|
+
};
|
|
1182
|
+
return fns;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Answers with an error page, locked down: no sniffing, no ETag, and a content security policy
|
|
1187
|
+
* that allows nothing, since the page carries a message that came from somewhere else.
|
|
1188
|
+
*
|
|
1189
|
+
* @param {any} request
|
|
1190
|
+
* @param {any} response
|
|
1191
|
+
* @param {any} err
|
|
1192
|
+
* @param {boolean} [checkEnv] whether production should redact it
|
|
1193
|
+
*/
|
|
1194
|
+
_sendErrorPage(request, response, err, checkEnv = false) {
|
|
1195
|
+
err = this._generateErrorPage(err, response.statusCode, checkEnv);
|
|
1196
|
+
request.noEtag = true;
|
|
1197
|
+
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1198
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1199
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'");
|
|
1200
|
+
response.send(err);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* How a request that nothing answered ends: with the error it carries, with the automatic
|
|
1205
|
+
* OPTIONS reply, or with a 404. The native chain, the app's catch-all handler and the node shim
|
|
1206
|
+
* all end here, so that they end a request the same way.
|
|
1207
|
+
*
|
|
1208
|
+
* @param {any} request
|
|
1209
|
+
* @param {any} response
|
|
1210
|
+
*/
|
|
1211
|
+
_endUnmatched(request, response) {
|
|
1212
|
+
if (request._error) {
|
|
1213
|
+
return this._handleError(request._error, null, request, response);
|
|
1214
|
+
}
|
|
1215
|
+
if (request._isOptions && request._matchedMethods.size > 0) {
|
|
1216
|
+
// Express 5 sorts the methods and joins them with ", ", so the header reads the same
|
|
1217
|
+
// regardless of the order the routes happened to be registered in
|
|
1218
|
+
const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
|
|
1219
|
+
response.setHeader("Allow", allowedMethods);
|
|
1220
|
+
// the router package answers this one itself, with a plain-text body, the nosniff
|
|
1221
|
+
// header and end() rather than send(), so no ETag comes with it
|
|
1222
|
+
response.setHeader("Content-Type", "text/plain");
|
|
1223
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1224
|
+
response.end(allowedMethods);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
response.status(404);
|
|
1228
|
+
// the whole path, not what a mount left behind in req.path
|
|
1229
|
+
this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// The verb methods go on the prototype, not on each instance. As own arrows they closed over the
|
|
1234
|
+
// instance they were built on, so express.Router().post(...) answered with the object the callable
|
|
1235
|
+
// was copied from. One closure per name for the process instead of one per router, too.
|
|
1236
|
+
for (const method of methods) {
|
|
1237
|
+
module.exports.prototype[method] = function (path, ...callbacks) {
|
|
1238
|
+
return this.createRoute(method, path, this, ...callbacks);
|
|
1239
|
+
};
|
|
1240
|
+
}
|