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.
@@ -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
- /** Called before the file goes out, to set headers from the path or its stat. */
46
- setHeaders?: (res: any, path: string, stat: any) => void;
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
- /** Runs on the raw bytes before parsing, which is where a signature check belongs. */
99
- verify?: false | ((req: any, res: any, buf: Buffer, encoding: string) => void);
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. */
@@ -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 the way body-parser's
41
- * parameterCount does, but inside this scan instead of a scan of its own: the overflow flag on
42
- * the function is set, the partial result is to be discarded, and the caller answers 413.
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]