fulmine.js 5.19.2 → 5.19.4
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/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/optimizer.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** @typedef {import("./router.js")} Router */
|
|
21
|
+
/** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
|
|
22
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
patternToRegex,
|
|
26
|
+
needsConversionToRegex,
|
|
27
|
+
canBeOptimized,
|
|
28
|
+
canBeOptimizedWithParams,
|
|
29
|
+
pathsCanOverlap,
|
|
30
|
+
uwsPrefersEarlier,
|
|
31
|
+
NullObject
|
|
32
|
+
} = require("./utils.js");
|
|
33
|
+
const Walk = require("./walk.js");
|
|
34
|
+
const compileDeclarative = require("./declarative.js");
|
|
35
|
+
const { chainUsage } = require("./usage.js");
|
|
36
|
+
const {
|
|
37
|
+
HAS_LETTER,
|
|
38
|
+
anyGuardHits,
|
|
39
|
+
resCodes,
|
|
40
|
+
resDecMethods,
|
|
41
|
+
nativeDone,
|
|
42
|
+
nativeFail,
|
|
43
|
+
nativePreset,
|
|
44
|
+
hasErrorMiddleware,
|
|
45
|
+
couldAnswer,
|
|
46
|
+
shadowsLeaf,
|
|
47
|
+
guardsInside,
|
|
48
|
+
supportedUwsMethods,
|
|
49
|
+
regExParam
|
|
50
|
+
} = require("./router-utils.js");
|
|
51
|
+
|
|
52
|
+
// router.js requires this file while it is still being evaluated, so its class cannot be required
|
|
53
|
+
// from here: it hands it over on the line under its own export instead.
|
|
54
|
+
let Router;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {typeof import("./router.js")} cls the Router class, passed in to keep this module out of its require cycle
|
|
58
|
+
*/
|
|
59
|
+
function useRouterClass(cls) {
|
|
60
|
+
Router = cls;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The chain a request would walk to reach this route, or false when it cannot be known ahead of
|
|
65
|
+
* time. The native router jumps straight to the route, so everything registered before it that
|
|
66
|
+
* could also match has to be in the chain, in order.
|
|
67
|
+
*
|
|
68
|
+
* @param {Router} router
|
|
69
|
+
* @param {RouteEntry} route
|
|
70
|
+
* @param {RouteEntry[]} routes every route of this router, in registration order
|
|
71
|
+
* @returns {RouteEntry[]|false} the chain, ending in the route itself
|
|
72
|
+
*/
|
|
73
|
+
function optimizeRoute(router, route, routes) {
|
|
74
|
+
const optimizedPath = [];
|
|
75
|
+
// a route with a parameter matches paths its own text does not, so what an earlier route
|
|
76
|
+
// could answer is compared shape against shape and not against that text
|
|
77
|
+
const withParams = typeof route.path === "string" && route.path.includes(":");
|
|
78
|
+
// under insensitive routing two paths that differ only in case answer the same requests, so
|
|
79
|
+
// the text comparisons below run on the folded form. uWS still matches bytes: a request in the
|
|
80
|
+
// registered case takes the chain, any other case takes the fallback
|
|
81
|
+
const caseSensitive = router._caseSensitive();
|
|
82
|
+
const routePathFolded = caseSensitive || typeof route.path !== "string" ? route.path : route.path.toLowerCase();
|
|
83
|
+
// whether this route answers only the path as written, or the one with a trailing slash too
|
|
84
|
+
const strictHere = (route.owner ?? router)._strictRouting();
|
|
85
|
+
/** @type {string[]|null} earlier literals a case variant could smuggle a request past */
|
|
86
|
+
let caseGuards = null;
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < routes.length; i++) {
|
|
89
|
+
const r = routes[i];
|
|
90
|
+
if (r.routeKey > route.routeKey) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (r === route) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// if the methods are not the same, and its not an all method, skip it
|
|
97
|
+
if (!r.all && r.method !== route.method) {
|
|
98
|
+
// check if the methods are compatible (GET and HEAD)
|
|
99
|
+
if (!(r.method === "HEAD" && route.method === "GET")) {
|
|
100
|
+
// A mount is registered ALL, because what lives under it can answer any method,
|
|
101
|
+
// and this chain is computed once for all of them. So an earlier route of another
|
|
102
|
+
// method belongs in the chain of the leaves that share its method and in no other,
|
|
103
|
+
// which one chain cannot say: uWS would jump to a leaf as if the earlier route did
|
|
104
|
+
// not exist. Leave the mount to ordinary dispatch, where express's order decides.
|
|
105
|
+
if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The same rule as above, reached by another road. A mount's chain is inherited by every
|
|
113
|
+
// path under it, and a route that is not a mount answers the mount point rather than the
|
|
114
|
+
// subtree: in the chain it ran for the whole of it, so router.all("/:p1") answered the
|
|
115
|
+
// /posts/a-b of the router mounted at /posts. guardsInside is written for this.
|
|
116
|
+
if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// a RegExp mount runs only where its match starts the path and breaks on a separator,
|
|
121
|
+
// which is decidable here against a literal path and not against one with a parameter
|
|
122
|
+
if (r.regexMount) {
|
|
123
|
+
const matched = typeof route.path === "string" ? r.pattern.exec(route.path) : null;
|
|
124
|
+
const runsAlways =
|
|
125
|
+
matched !== null &&
|
|
126
|
+
!matched[0].includes(":") &&
|
|
127
|
+
route.path.slice(0, matched[0].length) === matched[0] &&
|
|
128
|
+
(route.path.length === matched[0].length || route.path[matched[0].length] === "/");
|
|
129
|
+
if (runsAlways) {
|
|
130
|
+
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
optimizedPath.push(r);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// it may still answer some of the paths this route matches, and the chain has no
|
|
137
|
+
// way to say "only sometimes"
|
|
138
|
+
if (matched !== null || withParams) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// check if the paths match. A route with parameters is excluded from the text test:
|
|
145
|
+
// its literal ":name" text would let an earlier regex in on requests it never matches.
|
|
146
|
+
const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
|
|
147
|
+
if (
|
|
148
|
+
(regexCanMatch && r.pattern.test(route.path)) ||
|
|
149
|
+
(typeof r.pattern === "string" &&
|
|
150
|
+
(r.pattern === route.path ||
|
|
151
|
+
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
152
|
+
r.pattern === "/*"))
|
|
153
|
+
) {
|
|
154
|
+
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
155
|
+
return false; // cant optimize nested routers with matches
|
|
156
|
+
}
|
|
157
|
+
optimizedPath.push(r);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Without strict routing this registration answers "/x/" as well as "/x". An earlier
|
|
161
|
+
// pattern matching only the second answers part of what the registration takes, which the
|
|
162
|
+
// chain cannot say: it runs what is in it without matching again. So
|
|
163
|
+
// app.all("/:p0/{:o1}/{:o2}") answered a GET /list/Mixed belonging to the route after it.
|
|
164
|
+
if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
if (!withParams) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// an earlier route that answers only some of the paths this one matches cannot go in
|
|
171
|
+
// the chain, which runs what is in it without matching again
|
|
172
|
+
if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const rPathFolded = caseSensitive ? r.path : r.path.toLowerCase();
|
|
176
|
+
if (!pathsCanOverlap(rPathFolded, routePathFolded, r.use)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (r.use) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
// the same path lands on the same µWS registration, so the earlier route runs first
|
|
183
|
+
// from inside the chain, under its own parameter names; a case variant of it lands on
|
|
184
|
+
// its own registration, whose chain was computed the same way, or on the fallback
|
|
185
|
+
if (rPathFolded === routePathFolded) {
|
|
186
|
+
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
optimizedPath.push(r);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// otherwise the two overlap only where µWS itself hands the request to the earlier,
|
|
193
|
+
// more specific registration, so this chain never sees those paths
|
|
194
|
+
if (
|
|
195
|
+
!r.optimizedPath ||
|
|
196
|
+
!uwsPrefersEarlier(r.path, route.path) ||
|
|
197
|
+
(!caseSensitive && route.path !== routePathFolded)
|
|
198
|
+
) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
// that argument is about bytes. Under insensitive routing "/POSTS" byte-matches no
|
|
202
|
+
// registration of "/posts", so uWS hands it here, where this chain would answer as if the
|
|
203
|
+
// earlier route did not exist. The literal is remembered so the registration can send those
|
|
204
|
+
// requests to the generic router. A path with no letter has no other case to arrive in
|
|
205
|
+
if (!caseSensitive && HAS_LETTER.test(r.path)) {
|
|
206
|
+
(caseGuards ??= []).push(r.path);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
optimizedPath.push(route);
|
|
210
|
+
route._caseGuards = caseGuards;
|
|
211
|
+
|
|
212
|
+
return optimizedPath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Hands every route reachable by path alone to the native uWS router, walking into mounted
|
|
217
|
+
* routers and carrying their prefix down. Runs once, when the app starts listening, since it
|
|
218
|
+
* needs every route to have been registered first.
|
|
219
|
+
*
|
|
220
|
+
* @param {Router} root the application whose routes are being compiled. A plain router, which has
|
|
221
|
+
* no uwsApp, is left alone
|
|
222
|
+
*/
|
|
223
|
+
function compileOptimizedRoutes(root) {
|
|
224
|
+
if (!root.uwsApp) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Everything below is what makes this framework fast, and every decision it takes claims that
|
|
228
|
+
// uWS answering by itself gives the same answer the chain would. Turned off, the claim is not
|
|
229
|
+
// made. `npm run fuzz -- --self` serves one application both ways and compares the answers.
|
|
230
|
+
if (root.get("native routes") === false) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
235
|
+
// carries what was written before them and answers only part of what is under them
|
|
236
|
+
const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
|
|
237
|
+
for (const route of router._routes) {
|
|
238
|
+
if (route.use) {
|
|
239
|
+
// only sole-callback mounts. Case rules do not gate the walk: each level's
|
|
240
|
+
// _optimizeRoute guards its own routes under its own setting, and a request in
|
|
241
|
+
// another case takes the fallback, which honours the child's setting
|
|
242
|
+
if (
|
|
243
|
+
!route.complex &&
|
|
244
|
+
canBeOptimized(route.path) &&
|
|
245
|
+
route.path !== "/*" &&
|
|
246
|
+
route.callbacks.length === 1 &&
|
|
247
|
+
route.callbacks[0] instanceof Router
|
|
248
|
+
) {
|
|
249
|
+
let pathToMount = router._optimizeRoute(route, router._routes);
|
|
250
|
+
if (!pathToMount) {
|
|
251
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
pathToMount = pathToMount.slice(0, -1);
|
|
255
|
+
const guards = guardsInside(router, route, pathPrefix, pathToMount, outerGuards);
|
|
256
|
+
if (guards === null) {
|
|
257
|
+
route._whyGeneric = "a path written before it cannot be read segment by segment";
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
route._walkedInto = true;
|
|
261
|
+
walk(
|
|
262
|
+
route.callbacks[0],
|
|
263
|
+
pathPrefix + route.path,
|
|
264
|
+
[
|
|
265
|
+
...chainPrefix,
|
|
266
|
+
...pathToMount,
|
|
267
|
+
{
|
|
268
|
+
...route,
|
|
269
|
+
callbacks: [],
|
|
270
|
+
callbackKinds: [],
|
|
271
|
+
keepMount: true,
|
|
272
|
+
// mounted sub-apps become req.app during their dispatch, like express
|
|
273
|
+
mountApp:
|
|
274
|
+
route.callbacks[0].constructor.name === "Application"
|
|
275
|
+
? route.callbacks[0]
|
|
276
|
+
: undefined
|
|
277
|
+
}
|
|
278
|
+
],
|
|
279
|
+
guards
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
// said once here rather than at each condition above: a mount is walked into
|
|
283
|
+
// only when µWS can match its path on its own and it carries exactly one
|
|
284
|
+
// router, and those are the two things worth telling anyone about
|
|
285
|
+
route._whyGeneric = !(route.callbacks.length === 1 && route.callbacks[0] instanceof Router)
|
|
286
|
+
? "it is middleware rather than a single mounted router"
|
|
287
|
+
: "µWS cannot match this mount path on its own";
|
|
288
|
+
}
|
|
289
|
+
// µWS picks by specificity and Express by registration order, so the chain
|
|
290
|
+
// computed for whichever route µWS lands on runs everything that could have
|
|
291
|
+
// matched before it
|
|
292
|
+
} else if (
|
|
293
|
+
// parameters that are whole segments are matched by µWS the same way
|
|
294
|
+
(canBeOptimized(route.path) || canBeOptimizedWithParams(route.path)) &&
|
|
295
|
+
// Inside a mounted router, only when nothing after it could answer the same path.
|
|
296
|
+
// Asked of literal routes too, not only parameter ones: uWS picks by specificity
|
|
297
|
+
// where Express picks by registration order, and a chain carries only what runs in
|
|
298
|
+
// front of its route, so `router.get("/a", (req, res, next) => next())` before
|
|
299
|
+
// `router.get("/:x", ...)` left the mount and answered 404. Found by the fuzzer,
|
|
300
|
+
// replay with --seed 221940161 --rounds 1.
|
|
301
|
+
(!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)) &&
|
|
302
|
+
supportedUwsMethods.has(route.method)
|
|
303
|
+
) {
|
|
304
|
+
// something outside this router, written before the mount it is in, that could
|
|
305
|
+
// answer this exact path. µWS would jump here and never give it its turn
|
|
306
|
+
if (outerGuards.length > 0 && typeof route.path === "string") {
|
|
307
|
+
const absolute = pathPrefix + route.path;
|
|
308
|
+
const guard = outerGuards.find((g) => shadowsLeaf(g, absolute, route));
|
|
309
|
+
if (guard) {
|
|
310
|
+
route._whyGeneric = `${guard.path} is written before the mount it is in and answers the same paths`;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const leafPath = router._optimizeRoute(route, router._routes);
|
|
315
|
+
if (!leafPath) {
|
|
316
|
+
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// param route earlier in the same router would steal this static path
|
|
320
|
+
if (leafPath.length > 1) {
|
|
321
|
+
const shadow = leafPath[leafPath.length - 2];
|
|
322
|
+
if (
|
|
323
|
+
shadow &&
|
|
324
|
+
!shadow.use &&
|
|
325
|
+
shadow.method === route.method &&
|
|
326
|
+
shadow.path !== route.path &&
|
|
327
|
+
shadow.pattern instanceof RegExp
|
|
328
|
+
) {
|
|
329
|
+
route._whyGeneric = `the parameter route ${shadow.path} is written before it`;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// the prefix goes in whether or not the mount had a path: a pathless mount
|
|
334
|
+
// adds nothing to the path and everything to the chain, the middlewares in
|
|
335
|
+
// front of it and the mount entry that says where to resume
|
|
336
|
+
const chain = chainPrefix.length > 0 ? [...chainPrefix, ...leafPath] : leafPath;
|
|
337
|
+
if (pathPrefix) {
|
|
338
|
+
const registered = {
|
|
339
|
+
...route,
|
|
340
|
+
path: pathPrefix + route.path,
|
|
341
|
+
pattern: pathPrefix + route.path,
|
|
342
|
+
optimizedRouter: true
|
|
343
|
+
};
|
|
344
|
+
if (route._caseGuards) {
|
|
345
|
+
// compared against the whole path µWS matched, so they carry the mount
|
|
346
|
+
// prefix, folded along with the rest of it
|
|
347
|
+
registered._caseGuards = route._caseGuards.map((p) => pathPrefix + p);
|
|
348
|
+
}
|
|
349
|
+
root._registerUwsRoute(registered, chain);
|
|
350
|
+
// the chain holds the original object, so the request-time guard has to find
|
|
351
|
+
// the computed fields there, or a mounted param route extracts its params
|
|
352
|
+
// twice. The names match: the prefix is static and adds no parameter
|
|
353
|
+
route.optimizedParams = registered.optimizedParams;
|
|
354
|
+
route.optimizedPath = registered.optimizedPath;
|
|
355
|
+
// and what was decided about it, for the same reason: the copy is thrown
|
|
356
|
+
// away and the profile reads the route the application actually holds
|
|
357
|
+
route._native = registered._native;
|
|
358
|
+
} else {
|
|
359
|
+
root._registerUwsRoute(route, chain);
|
|
360
|
+
}
|
|
361
|
+
} else if (!supportedUwsMethods.has(route.method)) {
|
|
362
|
+
route._whyGeneric = `µWS does not serve ${route.method}`;
|
|
363
|
+
} else if (canBeOptimized(route.path) || canBeOptimizedWithParams(route.path)) {
|
|
364
|
+
// eligible but for the overlap test, which only applies inside a mount
|
|
365
|
+
route._whyGeneric = "a route after it in the same mounted router could answer the same paths";
|
|
366
|
+
} else {
|
|
367
|
+
route._whyGeneric = "µWS cannot match this path on its own";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
walk(root, "", [], []);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Hands one route to µWS, along with the chain of everything that has to run in front of it,
|
|
377
|
+
* and records that chain on the route so the handler can walk it.
|
|
378
|
+
*
|
|
379
|
+
* @param {Router} router
|
|
380
|
+
* @param {RouteEntry} route
|
|
381
|
+
* @param {RouteEntry[]} optimizedPath the routes to run, in order, ending with this one
|
|
382
|
+
*/
|
|
383
|
+
function registerUwsRoute(router, route, optimizedPath) {
|
|
384
|
+
let method = route.method.toLowerCase();
|
|
385
|
+
if (method === "all") {
|
|
386
|
+
method = "any";
|
|
387
|
+
} else if (method === "delete") {
|
|
388
|
+
method = "del";
|
|
389
|
+
}
|
|
390
|
+
if (route.path.includes(":")) {
|
|
391
|
+
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
392
|
+
}
|
|
393
|
+
// null for almost every route: only a parameter route with an earlier literal that a case
|
|
394
|
+
// variant could slip past carries one, see _optimizeRoute. Built once here, and matched
|
|
395
|
+
// insensitively, since that is the folding the guard exists for
|
|
396
|
+
const caseGuards = route._caseGuards
|
|
397
|
+
? route._caseGuards.map((p) => (needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()))
|
|
398
|
+
: null;
|
|
399
|
+
const makeHandler = (chain, preset, skips, wireMethod) => {
|
|
400
|
+
// the mutable object a granted skip lives on, so a middleware arriving after listen can
|
|
401
|
+
// take it back: a literal registration's preset doubles as it, a parameterised one gets a
|
|
402
|
+
// holder of its own. It carries the method too, so the constructor settles it in one compare
|
|
403
|
+
let skipHolder = preset;
|
|
404
|
+
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
|
|
405
|
+
skipHolder = {
|
|
406
|
+
skipHeaders: skips.skipHeaders,
|
|
407
|
+
skipQuery: skips.skipQuery,
|
|
408
|
+
method: wireMethod,
|
|
409
|
+
isOptions: wireMethod === "OPTIONS",
|
|
410
|
+
isHead: wireMethod === "HEAD"
|
|
411
|
+
};
|
|
412
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
413
|
+
(router._skipPresets ??= new Set()).add(skipHolder);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// all three are registration-time constants: computing them in the handler was a
|
|
417
|
+
// closure and a scan of the chain on every native request.
|
|
418
|
+
// Falling back resumes after the mount, not after the router's leaf: the leaf can have
|
|
419
|
+
// a lower routeKey than the parent's middlewares, and an error handler declared before
|
|
420
|
+
// the mount must not catch what the router threw
|
|
421
|
+
const mount = chain.find((r) => r.keepMount);
|
|
422
|
+
const skipUntil = mount ?? chain[chain.length - 1];
|
|
423
|
+
const optimizedParams = route.optimizedParams;
|
|
424
|
+
// not async, and no _routeRequest: its promise pair exists for callers that await,
|
|
425
|
+
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
426
|
+
// microtask, which is where the await used to resume, so the visible order holds
|
|
427
|
+
return (res, req) => {
|
|
428
|
+
// a request that is an earlier literal in another case: express answers it with
|
|
429
|
+
// that route, and the chain here does not contain it, so the generic router takes
|
|
430
|
+
// this one
|
|
431
|
+
if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
|
|
432
|
+
// an application is what registers native routes, and only it serves
|
|
433
|
+
return /** @type {Application} */ (router)._serveGeneric(res, req);
|
|
434
|
+
}
|
|
435
|
+
const request = router.handleRequest(res, req, preset, skipHolder);
|
|
436
|
+
const response = request.res;
|
|
437
|
+
if (request._mustRefuse === true) {
|
|
438
|
+
return router._refuseRequest(response);
|
|
439
|
+
}
|
|
440
|
+
if (optimizedParams) {
|
|
441
|
+
// slicing these out of the already-fetched path instead measured a wash:
|
|
442
|
+
// the segment scan costs what the crossing costs
|
|
443
|
+
request.optimizedParams = new NullObject();
|
|
444
|
+
for (let i = 0; i < optimizedParams.length; i++) {
|
|
445
|
+
request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const walk = new Walk(router, request, response, chain, true, skipUntil, nativeDone, nativeFail);
|
|
449
|
+
try {
|
|
450
|
+
walk.dispatch(0);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
// what a throw inside a promise executor did: reject, once
|
|
453
|
+
nativeFail.call(walk, err);
|
|
454
|
+
} finally {
|
|
455
|
+
// whatever runs after this line is outside the cork uWS held for this
|
|
456
|
+
// callback, so later writes have to open their own
|
|
457
|
+
response._corkNeeded = true;
|
|
458
|
+
// an abort can only arrive after this callback returns, so a response that
|
|
459
|
+
// already finished inside it never needs uWS told at all
|
|
460
|
+
if (!response.finished) {
|
|
461
|
+
router._armAbort(res, response);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
// a HEAD route may sit in a GET route's chain so the head registration runs it, but a
|
|
467
|
+
// chain runs without re-matching the method, so the get registration must not see it
|
|
468
|
+
const getChain = route.method === "GET" ? optimizedPath.filter((r) => r.all || r.method !== "HEAD") : optimizedPath;
|
|
469
|
+
route.optimizedPath = optimizedPath;
|
|
470
|
+
const headChain = getChain.length === optimizedPath.length ? getChain : optimizedPath;
|
|
471
|
+
|
|
472
|
+
// A fully literal registration knows path and method here, so each registration site
|
|
473
|
+
// hands the request constructor its own constants. An "any" registration serves every
|
|
474
|
+
// verb and a parameterised one matches paths it cannot spell, so both stay dynamic
|
|
475
|
+
const canPreset = !route.optimizedParams && method !== "any";
|
|
476
|
+
// the route's own router decides, not the app running the registration: a router created
|
|
477
|
+
// with { strict: true } and mounted on an app without it does not answer /things/, and
|
|
478
|
+
// registering that path here is the only way it could
|
|
479
|
+
const strictHere = (route.owner ?? router)._strictRouting();
|
|
480
|
+
|
|
481
|
+
// Whether requests served by this registration may skip the header copy: GET and its HEAD twins
|
|
482
|
+
// only, no error middleware anywhere (a throw hands the request to code the analysis never
|
|
483
|
+
// saw), and every callback in the chain has to pass usage.js, whose default answer is no.
|
|
484
|
+
//
|
|
485
|
+
// The etag setting is not a condition. It used to be, because send consults freshness, but the
|
|
486
|
+
// skip branch reads if-none-match and if-modified-since by name whatever the setting, see the
|
|
487
|
+
// comment in request.js, and req.fresh reads nothing else. Requiring etag off as well cost the
|
|
488
|
+
// copy to every application that left it on.
|
|
489
|
+
const NO_SKIPS = { skipHeaders: false, skipQuery: false };
|
|
490
|
+
let getSkips = NO_SKIPS;
|
|
491
|
+
let headSkips = NO_SKIPS;
|
|
492
|
+
if (route.method === "GET") {
|
|
493
|
+
let hasErr = router._hasErrMwCache;
|
|
494
|
+
if (hasErr === undefined) {
|
|
495
|
+
hasErr = router._hasErrMwCache = hasErrorMiddleware(router);
|
|
496
|
+
}
|
|
497
|
+
if (!hasErr) {
|
|
498
|
+
// a terminal next() may only fall into the framework's own 404, so no later
|
|
499
|
+
// route may be able to catch the same path
|
|
500
|
+
const owner = route.owner ?? router;
|
|
501
|
+
const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
|
|
502
|
+
getSkips = chainUsage(getChain, noLaterMatch);
|
|
503
|
+
headSkips = headChain === getChain ? getSkips : chainUsage(headChain, noLaterMatch);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
507
|
+
const makePreset = (path, method, skips) => {
|
|
508
|
+
const preset = nativePreset(path, method);
|
|
509
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
510
|
+
preset.skipHeaders = skips.skipHeaders;
|
|
511
|
+
preset.skipQuery = skips.skipQuery;
|
|
512
|
+
(router._skipPresets ??= new Set()).add(preset);
|
|
513
|
+
}
|
|
514
|
+
return preset;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// the wire token this registration answers; "any" serves every verb and stays dynamic
|
|
518
|
+
const wireMethod = method === "any" ? null : route.method;
|
|
519
|
+
let fn = makeHandler(
|
|
520
|
+
getChain,
|
|
521
|
+
canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
|
|
522
|
+
getSkips,
|
|
523
|
+
wireMethod
|
|
524
|
+
);
|
|
525
|
+
const jsFn = fn;
|
|
526
|
+
|
|
527
|
+
let replacedPath = route.path;
|
|
528
|
+
|
|
529
|
+
// the response prototype the route will really run under: its own app's, which sees a
|
|
530
|
+
// method patched there or inherited from a parent app, falling back to the registering app
|
|
531
|
+
const responseProto = route.owner?.response ?? /** @type {Application} */ (router).response;
|
|
532
|
+
// check if route is declarative
|
|
533
|
+
if (
|
|
534
|
+
optimizedPath.length === 1 && // must not have middlewares
|
|
535
|
+
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
536
|
+
typeof route.callbacks[0] === "function" && // must be a function
|
|
537
|
+
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
538
|
+
// a captured value is decoded when the route runs, and one that cannot be decoded is a
|
|
539
|
+
// 400 in express and on the ordinary path here. Nothing runs to raise it on a
|
|
540
|
+
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
|
|
541
|
+
route.optimizedParams === undefined &&
|
|
542
|
+
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
543
|
+
// guard could not: a route that needs one has to stay an ordinary handler
|
|
544
|
+
caseGuards === null &&
|
|
545
|
+
!resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
|
|
546
|
+
router.get("declarative responses") // must have declarative responses enabled
|
|
547
|
+
) {
|
|
548
|
+
const decRes = compileDeclarative(route.callbacks[0], router);
|
|
549
|
+
if (decRes) {
|
|
550
|
+
fn = decRes;
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
replacedPath = route.path.replace(regExParam, ":x");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// what listen() settled about this route, kept so `npx fulmine profile` can print it rather
|
|
557
|
+
// than making anyone read the source or instrument it. Written once, during compilation,
|
|
558
|
+
// so no request pays for it
|
|
559
|
+
route._native = {
|
|
560
|
+
path: replacedPath,
|
|
561
|
+
declarative: fn !== jsFn,
|
|
562
|
+
skipHeaders: getSkips.skipHeaders === true,
|
|
563
|
+
skipQuery: getSkips.skipQuery === true,
|
|
564
|
+
ahead: optimizedPath.length - 1,
|
|
565
|
+
guards: caseGuards ? caseGuards.length : 0
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
router.uwsApp[method](replacedPath, fn);
|
|
569
|
+
if (!strictHere && route.path[route.path.length - 1] !== "/") {
|
|
570
|
+
// a declarative response answers the twin as itself; a preset handler cannot be
|
|
571
|
+
// shared, since the twin's path is its own constant
|
|
572
|
+
const slashFn =
|
|
573
|
+
fn !== jsFn
|
|
574
|
+
? fn
|
|
575
|
+
: canPreset
|
|
576
|
+
? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips, wireMethod)
|
|
577
|
+
: fn;
|
|
578
|
+
router.uwsApp[method](replacedPath + "/", slashFn);
|
|
579
|
+
if (method === "get") {
|
|
580
|
+
router.uwsApp.head(
|
|
581
|
+
replacedPath + "/",
|
|
582
|
+
makeHandler(
|
|
583
|
+
headChain,
|
|
584
|
+
canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
|
|
585
|
+
headSkips,
|
|
586
|
+
"HEAD"
|
|
587
|
+
)
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (method === "get") {
|
|
592
|
+
// its own handler always: the shared one would carry the GET registration's method
|
|
593
|
+
router.uwsApp.head(
|
|
594
|
+
replacedPath,
|
|
595
|
+
makeHandler(headChain, canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined, headSkips, "HEAD")
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = { useRouterClass, optimizeRoute, compileOptimizedRoutes, registerUwsRoute };
|
package/src/options.d.ts
CHANGED
|
@@ -42,8 +42,8 @@ export interface SendFileOptions {
|
|
|
42
42
|
dotfiles?: "allow" | "deny" | "ignore" | "ignore_files";
|
|
43
43
|
/** Extra headers for the response. */
|
|
44
44
|
headers?: Record<string, string>;
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/** Internal: express.static's setHeaders, which res.sendFile itself does not take. */
|
|
46
|
+
_setHeaders?: (res: any, path: string, stat: any) => void;
|
|
47
47
|
/** First byte of the window to send. */
|
|
48
48
|
start?: number;
|
|
49
49
|
/** Last byte of the window to send. */
|
|
@@ -68,6 +68,8 @@ export interface StaticOptions extends SendFileOptions {
|
|
|
68
68
|
fallthrough?: boolean;
|
|
69
69
|
/** Extensions tried when the path names no file, or false to try none. */
|
|
70
70
|
extensions?: string[] | false;
|
|
71
|
+
/** Called before the file goes out, to set headers from the path or its stat. */
|
|
72
|
+
setHeaders?: (res: any, path: string, stat: any) => void;
|
|
71
73
|
/**
|
|
72
74
|
* Serve `file.br` or `file.gz` in place of `file` when one is on disk and the client takes it.
|
|
73
75
|
* Off by default. Vary: Accept-Encoding is sent whether or not a variant is found, and the
|
|
@@ -95,8 +97,11 @@ export interface BodyParserOptions {
|
|
|
95
97
|
limit?: number | string;
|
|
96
98
|
/** Which content types this parser claims. */
|
|
97
99
|
type?: string | string[] | ((req: any) => boolean);
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Runs on the raw bytes before parsing, which is where a signature check belongs. The charset
|
|
102
|
+
* is the one the body is decoded with, null for raw, as body-parser hands it over.
|
|
103
|
+
*/
|
|
104
|
+
verify?: false | ((req: any, res: any, buf: Buffer, encoding: string | null) => void);
|
|
100
105
|
/** Whether a compressed body is decompressed rather than refused. */
|
|
101
106
|
inflate?: boolean;
|
|
102
107
|
/** The charset assumed when the request names none. */
|
package/src/parse-query.js
CHANGED
|
@@ -37,9 +37,9 @@ const plusRegex = /\+/g;
|
|
|
37
37
|
* `capture` collects the decoded pairs flat, key then value, so a caller can replay the stores
|
|
38
38
|
* without scanning again; a repeated key marks it invalid instead. See `get query`.
|
|
39
39
|
*
|
|
40
|
-
* `separatorLimit` refuses a body with that many "&" separators
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* `separatorLimit` refuses a body with that many "&" separators like body-parser's parameterCount,
|
|
41
|
+
* inside this scan: the overflow flag is set, the partial result is discarded, the caller answers
|
|
42
|
+
* 413.
|
|
43
43
|
*
|
|
44
44
|
* @param {string} input
|
|
45
45
|
* @param {string[] & {invalid?: boolean}} [capture]
|