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/src/walk.js ADDED
@@ -0,0 +1,581 @@
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
+ const {
21
+ mountPrefixLength,
22
+ setMountedPath,
23
+ stepsOver,
24
+ rememberApp,
25
+ restoreApp,
26
+ useApp,
27
+ CALLBACK_ERROR,
28
+ CALLBACK_ROUTER
29
+ } = require("./router-utils.js");
30
+
31
+ /** @typedef {import("./request.js")} Request */
32
+ /** @typedef {import("./response.js")} Response */
33
+ /** @typedef {import("./router.js")} Router */
34
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
35
+
36
+ /**
37
+ * One walk of one router's routes, for one request.
38
+ *
39
+ * next() is made once here instead of once per hop. As a closure per hop it captured eleven
40
+ * bindings, one of them mutable, so a heap context every time a middleware handed over. The hop's
41
+ * own state is three fields on this instead.
42
+ *
43
+ * A nested router gets its own walk, so req.next belongs to whoever is running the request.
44
+ */
45
+ class Walk {
46
+ /**
47
+ * @param {Router} router
48
+ * @param {Request} req
49
+ * @param {Response} res
50
+ * @param {RouteEntry[]} routes the route table being walked, see createRoute in router.js
51
+ * @param {boolean} skipCheck take the route at the index without matching it, which is how an
52
+ * already-decided chain is walked
53
+ * @param {RouteEntry|undefined} skipUntil route to resume after when this chain runs out, or undefined
54
+ * @param {(value: RouteEntry|false) => void} resolve
55
+ * @param {(err: unknown) => void} reject
56
+ */
57
+ constructor(router, req, res, routes, skipCheck, skipUntil, resolve, reject) {
58
+ this.router = router;
59
+ this.req = req;
60
+ this.res = res;
61
+ this.routes = routes;
62
+ this.skipCheck = skipCheck;
63
+ this.skipUntil = skipUntil;
64
+ this.resolve = resolve;
65
+ this.reject = reject;
66
+ // read only by the native pair below, which has no promise to settle once for it; the
67
+ // promise path leaves it false. Initialized here to keep every walk the same shape
68
+ this.settled = false;
69
+ this.routeIndex = 0;
70
+ this.route = null;
71
+ this.callbackIndex = 0;
72
+ // bound, not wrapped in an arrow: an arrow forwarding into step() is one more call on every
73
+ // hop, and it measured 495 microseconds per thousand requests of nothing else
74
+ this.next = this.step.bind(this);
75
+ // What res.sendFile reports a failure to. Express hands it req.next, the router next and
76
+ // not the route one, so a file that cannot be served leaves the route and its error reaches
77
+ // the router error handlers. req.next itself is left alone: making it mean this everywhere
78
+ // breaks express own res.format and app.routes.error tests here.
79
+ //
80
+ // Null here and bound on the first route with more than one callback, the only shape that
81
+ // reads it: a request that never meets one paid a bind for nothing
82
+ this.leaveRoute = null;
83
+ }
84
+
85
+ /**
86
+ * Leaves the rest of this route, with the error if there is one, and carries on with the route
87
+ * after it.
88
+ *
89
+ * @param {unknown} [err] whatever was thrown, which need not be an Error
90
+ */
91
+ stepOutOfRoute(err) {
92
+ if (err) {
93
+ const req = this.req;
94
+ req._error = err;
95
+ req._errorKey = this.route.routeKey;
96
+ req._errorGroup = this.route.group;
97
+ }
98
+ this.step("route");
99
+ }
100
+
101
+ /**
102
+ * Finds the next route that matches and runs it. next() comes back here for the route after, so
103
+ * a chain of N middlewares costs one promise instead of N nested ones.
104
+ *
105
+ * @param {number} startIndex where to resume the scan
106
+ */
107
+ dispatch(startIndex) {
108
+ const req = this.req;
109
+ const routes = this.routes;
110
+ const router = this.router;
111
+ // a middleware assigned req.url, which express honours: the rest of the walk matches the
112
+ // new path. One identity compare per hop, since the router writes both sides itself; the
113
+ // handling lives out of line so this function stays small enough to inline
114
+ if (req.url !== req._lastUrl && this.takeUrlRewrite(startIndex)) {
115
+ return;
116
+ }
117
+ // and the same for req.method, which method-override assigns: the compiled chain was
118
+ // picked by the verb the request arrived with, so it no longer stands for this one
119
+ if (req.method !== req._lastMethod && this.takeMethodRewrite(startIndex)) {
120
+ return;
121
+ }
122
+ let routeIndex = startIndex;
123
+ // a compiled chain runs what is in it without matching again, so this is where a layer that
124
+ // provably has nothing to do for this request is stepped over rather than entered
125
+ if (this.skipCheck) {
126
+ while (routeIndex < routes.length && routes[routeIndex].bodyParserOnly === true) {
127
+ if (!stepsOver(routes[routeIndex], req)) {
128
+ break;
129
+ }
130
+ routeIndex++;
131
+ }
132
+ }
133
+ if (!this.skipCheck) {
134
+ // express matches a layer's path before the method and decodes the parameters there, so
135
+ // a malformed escape answers 400 even when no route of this method exists. Only a path
136
+ // with a percent can produce one. Scanned once per rewrite and kept on the request
137
+ const mayFailDecode = (req._mayFailDecode ??= req._originalPath.indexOf("%") !== -1);
138
+ // frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
139
+ // calling this per route measured 0.45us of a scan of four hundred
140
+ router._freezeRoutingFlags();
141
+ if (routes === router._routes) {
142
+ // the router's own table has an index over its literal routes, so the scan visits
143
+ // the handful that could match instead of every one, see _scanFrom
144
+ routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
145
+ } else {
146
+ // a compiled chain's own array, always short: the linear scan stays.
147
+ // Written out rather than through a predicate handed to findIndexStartingFrom,
148
+ // which was one closure per hop of every request not on a compiled chain
149
+ const method = req.method;
150
+ const length = routes.length;
151
+ for (; routeIndex < length; routeIndex++) {
152
+ const r = routes[routeIndex];
153
+ // A HEAD request enters a route whose path matched even when its verb cannot
154
+ // serve one: express exempts HEAD from the method check ("if (!hasMethod &&
155
+ // method !== 'HEAD')" in router/index.js), so the layer's parameters are
156
+ // captured and its param() callbacks run first. Only asked when the router has
157
+ // callbacks to run. runRoute steps over it.
158
+ if (!(
159
+ r.all ||
160
+ r.method === method ||
161
+ req._isOptions ||
162
+ (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
163
+ )) {
164
+ // taken only to fail: _preprocessRequest decodes again and turns it into
165
+ // the error, so the handlers of a route this request cannot run never see it
166
+ if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
167
+ break;
168
+ }
169
+ continue;
170
+ }
171
+ if (router._pathMatches(r, req)) {
172
+ // matched, and then stepped over: a body parser this request gets nothing
173
+ // out of costs a hop and answers with next() at the end of it
174
+ if (r.bodyParserOnly === true && stepsOver(r, req)) {
175
+ continue;
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ const route = routes[routeIndex];
183
+ if (!route) {
184
+ if (!this.skipCheck) {
185
+ // on normal unoptimized routes, if theres no match then there is no route
186
+ return this.resolve(false);
187
+ }
188
+ // the chain ran out, so ordinary routing takes over from the top and skips what has
189
+ // already run
190
+ useApp(req, router);
191
+ // a chain that went into a mount never left it, since keepMount stops the pop, so the
192
+ // path is still relative to it. /alone/skip must not be offered to the app as /skip
193
+ if (req._stack !== null && req._stack.length > 0) {
194
+ req._stack.length = 0;
195
+ req._consumed = 0;
196
+ setMountedPath(req);
197
+ }
198
+ // an error out of a mount is attributed to the mount, so error handlers declared before
199
+ // it do not catch it, as in ordinary dispatch
200
+ if (
201
+ req._error &&
202
+ this.skipUntil &&
203
+ this.skipUntil.keepMount &&
204
+ this.skipUntil.routeKey > /** @type {number} */ (req._errorKey)
205
+ ) {
206
+ req._errorKey = this.skipUntil.routeKey;
207
+ req._errorGroup = this.skipUntil.group;
208
+ }
209
+ this.routes = router._routes;
210
+ this.skipCheck = false;
211
+ return this.dispatch(0);
212
+ }
213
+
214
+ this.routeIndex = routeIndex;
215
+ this.route = route;
216
+ this.callbackIndex = 0;
217
+
218
+ // _preprocessRequest returns a promise only when param callbacks will really run, so the
219
+ // common case stays synchronous even in an app that uses app.param. A microtask every 300
220
+ // routes resets the stack, which a long chain would otherwise blow
221
+ const continueRoute = router._preprocessRequest(req, this.res, route);
222
+ if (continueRoute instanceof Promise || req.routeCount % 300 === 0) {
223
+ // .catch and not a rejection argument: a throw inside runRoute itself must reject
224
+ // the walk instead of becoming an unhandled rejection
225
+ Promise.resolve(continueRoute)
226
+ .then((resumed) => this.runRoute(resumed))
227
+ // wrapped so the native pair keeps the walk as receiver; a promise's reject
228
+ // would not have cared
229
+ .catch((err) => this.reject(err));
230
+ return;
231
+ }
232
+ return this.runRoute(continueRoute);
233
+ }
234
+
235
+ /**
236
+ * Takes over a req.url a middleware assigned. On an ordinary walk the scan simply continues
237
+ * against the new path; a compiled chain was computed for the old one, so ordinary routing
238
+ * takes over, skipping only what has already run.
239
+ *
240
+ * @param {number} startIndex where dispatch was about to resume
241
+ * @returns {boolean} whether this rerouted the walk itself
242
+ */
243
+ takeUrlRewrite(startIndex) {
244
+ const req = this.req;
245
+ const router = this.router;
246
+ req._absorbUrlRewrite();
247
+ if (!this.skipCheck) {
248
+ return false;
249
+ }
250
+ this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
251
+ if (req._stack !== null && req._stack.length > 0) {
252
+ req._stack.length = 0;
253
+ req._consumed = 0;
254
+ setMountedPath(req);
255
+ }
256
+ this.routes = router._routes;
257
+ this.skipCheck = false;
258
+ this.dispatch(0);
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * Takes over a req.method a middleware assigned, which method-override does. The ordinary scan
264
+ * reads req.method per route and is right from the next hop. A compiled chain was chosen by the
265
+ * method uWS dispatched on, so ordinary routing takes over from the top.
266
+ *
267
+ * @param {number} startIndex where dispatch was about to resume
268
+ * @returns {boolean} whether this rerouted the walk itself
269
+ */
270
+ takeMethodRewrite(startIndex) {
271
+ const req = this.req;
272
+ const router = this.router;
273
+ req._absorbMethodRewrite();
274
+ if (!this.skipCheck) {
275
+ return false;
276
+ }
277
+ this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
278
+ if (req._stack !== null && req._stack.length > 0) {
279
+ req._stack.length = 0;
280
+ req._consumed = 0;
281
+ setMountedPath(req);
282
+ }
283
+ this.routes = router._routes;
284
+ this.skipCheck = false;
285
+ this.dispatch(0);
286
+ return true;
287
+ }
288
+
289
+ /**
290
+ * Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
291
+ * way in, and then the route's callbacks run one after another through next().
292
+ *
293
+ * @param {true|"route"} continueRoute what _preprocessRequest decided: true to run, "route" to skip
294
+ */
295
+ runRoute(continueRoute) {
296
+ const req = this.req;
297
+ const route = this.route;
298
+ // A compiled chain walks into a mount rather than entering it, so the rule above needs
299
+ // saying here too: everything after this marker is inside the mount, and a mount is stepped
300
+ // over while an error is in flight. Ordinary routing takes over after the mount.
301
+ if (route.keepMount === true && req._error) {
302
+ return this.dispatch(this.routes.length);
303
+ }
304
+ if (route.use) {
305
+ if (route.mountApp) {
306
+ // optimized chain: normal dispatch swaps req.app when it enters a mounted
307
+ // Application, but the compiled mount route has no callback to do it
308
+ rememberApp(this, route, req);
309
+ useApp(req, route.mountApp);
310
+ }
311
+ const taken = mountPrefixLength(route, req);
312
+ // pushed negative when this mount consumes the whole remaining path: express invents
313
+ // the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
314
+ (req._stack ??= []).push(
315
+ taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
316
+ );
317
+ // a use with no path consumes nothing, so everything below would work out the values
318
+ // that are already there. Only skipped without a trailing slash, where the rules about
319
+ // one cannot bite. An application is mostly pathless middleware, and this is per hop
320
+ if (taken !== 0 || req.endsWithSlash) {
321
+ req._consumed += taken;
322
+ // a mount that took a trailing slash: req.baseUrl then has to join the pieces
323
+ // rather than slice the path, which is the slower half of its getter
324
+ if (taken !== 0 && req._originalPath.charCodeAt(req._consumed - 1) === 0x2f) {
325
+ req._mountSlash = true;
326
+ }
327
+ setMountedPath(req);
328
+ }
329
+ }
330
+ req.next = this.next;
331
+ // the same step when the route has one callback, and then it has to be the same object:
332
+ // express hands res.format's handlers the next its own layer received, and its test asserts
333
+ // that identity. With more than one callback express hands over the one that leaves the route
334
+ req._leaveRoute = route.callbacks.length > 1 ? (this.leaveRoute ??= this.stepOutOfRoute.bind(this)) : this.next;
335
+ if (continueRoute === "route") {
336
+ this.step("route");
337
+ } else if (continueRoute) {
338
+ this.step(undefined);
339
+ } else {
340
+ this.resolve(true);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * A hop while the request carries an error, or over an error handler it cannot run: the
346
+ * handler is invoked when the error is its to catch, everything else is skipped.
347
+ *
348
+ * @param {number} kind what the callback is, one of the CALLBACK_ constants
349
+ * @param {Function} callback
350
+ */
351
+ errorHop(kind, callback) {
352
+ const req = this.req;
353
+ const route = this.route;
354
+ // A four argument handler written inside a route only sees what that route raised: express
355
+ // skips a route layer while an error is in flight, so an error from a middleware before it,
356
+ // or out of a mount, walks past to the router's own error handlers. Middleware error
357
+ // handlers keep the ordinary rule.
358
+ const reachable = route.use
359
+ ? req._errorKey !== undefined && route.routeKey >= req._errorKey
360
+ : route.routeKey === req._errorKey || (route.group !== undefined && route.group === req._errorGroup);
361
+ if (req._error && kind === CALLBACK_ERROR && reachable) {
362
+ const out = this.router._handleError(req._error, callback, req, this.res);
363
+ if (out instanceof Promise) {
364
+ // an error handler's rejected promise moves on to the next error handler, and
365
+ // a bare rejection gets the error express invents for it
366
+ out.catch((err) => {
367
+ req._error = err || new Error("Rejected promise");
368
+ req._errorKey = route.routeKey;
369
+ req._errorGroup = route.group;
370
+ return this.step(undefined);
371
+ });
372
+ }
373
+ return;
374
+ }
375
+ return this.step(undefined);
376
+ }
377
+
378
+ /**
379
+ * Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
380
+ * route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
381
+ * here without re-running step's prologue and compares.
382
+ *
383
+ * @param {boolean} isRouter next("router") rather than next("route")
384
+ */
385
+ leaveHop(isRouter) {
386
+ const req = this.req;
387
+ const route = this.route;
388
+ if (route.use && !route.keepMount) {
389
+ const pushed = req._stack.pop();
390
+ const taken = pushed < 0 ? -pushed : pushed;
391
+ // a rewrite done inside this middleware is taken now: the pop below recomputes
392
+ // req.url from the original path and would silently revert it. The slashAdded
393
+ // mangle belongs to the mount that consumed a prefix, not to a pathless use
394
+ if (req.url !== req._lastUrl) {
395
+ req._absorbUrlRewrite(taken !== 0);
396
+ req._consumed -= taken;
397
+ setMountedPath(req);
398
+ } else {
399
+ if (pushed < 0 && req._originalPath.length > req._consumed) {
400
+ // a rewrite below this mount left a remainder where entry had none: express
401
+ // strips the first character of it when it rejoins, see issue #17
402
+ req._originalPath =
403
+ req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
404
+ req._mayFailDecode = null;
405
+ }
406
+ if (taken !== 0) {
407
+ // a pathless use consumed nothing and rewrote nothing, so the recompute would
408
+ // write back the very values it reads
409
+ req._consumed -= taken;
410
+ setMountedPath(req);
411
+ }
412
+ }
413
+ restoreApp(route, req);
414
+ }
415
+ if (isRouter) {
416
+ if (this.skipCheck) {
417
+ // on a compiled chain, leaving the router is what running out of chain already
418
+ // means: ordinary routing takes over after the mount. With no mount in the chain
419
+ // the router being left is the app's own, and nothing of it may run afterwards
420
+ if (this.skipUntil?.keepMount) {
421
+ return this.dispatch(this.routes.length);
422
+ }
423
+ return this.resolve(false);
424
+ }
425
+ // out of this router entirely, so whoever mounted it carries on after the
426
+ // mount. The app's own walk has nobody after it, and answers 404
427
+ return this.resolve(false);
428
+ }
429
+ req.routeCount++;
430
+ // dispatch is a plain call, so a synchronous throw would escape here instead of
431
+ // rejecting, as it used to when this recursed through the async _routeRequest
432
+ try {
433
+ return this.dispatch(this.routeIndex + 1);
434
+ } catch (err) {
435
+ return this.reject(err);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * One hop, which is what next() does: with nothing, run the route's next callback; with "route",
441
+ * leave the route; with anything else, remember it as the error and carry on.
442
+ *
443
+ * @param {unknown} thingamabob what next() was called with: nothing, "route", or an error
444
+ */
445
+ step(thingamabob) {
446
+ const req = this.req;
447
+ const res = this.res;
448
+ const route = this.route;
449
+ const router = this.router;
450
+ if (thingamabob) {
451
+ if (thingamabob === "route" || thingamabob === "router") {
452
+ return this.leaveHop(thingamabob === "router");
453
+ } else {
454
+ req._error = thingamabob;
455
+ req._errorKey = route.routeKey;
456
+ req._errorGroup = route.group;
457
+ }
458
+ }
459
+ const kind = route.callbackKinds[this.callbackIndex];
460
+ const callback = route.callbacks[this.callbackIndex++];
461
+ if (!callback) {
462
+ return this.leaveHop(false);
463
+ }
464
+ // skipping routes we already went through via optimized path. Before the Router branch
465
+ // below and not after it: a mount whose chain was compiled has already run, and running it
466
+ // again would answer from inside the router a request that had just left it
467
+ if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
468
+ return this.step(undefined);
469
+ }
470
+ // A mounted router or application is stepped over while an error is in flight. Its handle
471
+ // takes three arguments, so express's Layer#handleError hands the error straight on: what a
472
+ // mount catches is what it raised itself. Entering it left req.app pointing at the mounted
473
+ // application, whose settings answered, and a 500 carried an ETag under etag false.
474
+ if (kind === CALLBACK_ROUTER && !req._error) {
475
+ if (callback._isApplication) {
476
+ rememberApp(this, route, req);
477
+ useApp(req, callback);
478
+ }
479
+ const pushedParams = callback._settings.mergeParams;
480
+ if (pushedParams) {
481
+ (req._paramStack ??= []).push(req.params);
482
+ }
483
+ // express restores req.params when a router hands back, so what runs after the mount
484
+ // sees the params it had before it
485
+ const parentParams = req.params;
486
+ // each router answers OPTIONS with the verbs it knows itself, so the one being entered
487
+ // starts its own list: express keeps that list per router, and a router that hands back
488
+ // without answering leaves the outer one's untouched
489
+ const parentMethods = req._matchedMethods;
490
+ if (parentMethods !== null) {
491
+ req._matchedMethods = new Set();
492
+ }
493
+ callback
494
+ ._routeRequest(req, res, 0)
495
+ .then((routed) => {
496
+ // the child's params are scoped to it, and must not leak into the routes after
497
+ if (pushedParams) {
498
+ req._paramStack.pop();
499
+ }
500
+ req.params = parentParams;
501
+ if (req._error) {
502
+ req._errorKey = route.routeKey;
503
+ req._errorGroup = route.group;
504
+ }
505
+ if (routed) {
506
+ if (parentMethods !== null) {
507
+ req._matchedMethods = parentMethods;
508
+ }
509
+ return this.resolve(true);
510
+ }
511
+ const childMethods = req._matchedMethods;
512
+ if (parentMethods !== null) {
513
+ req._matchedMethods = parentMethods;
514
+ }
515
+ if (req._isOptions && childMethods !== null && childMethods.size && !req._error) {
516
+ // OPTIONS routing is different, it stops in the router if matched.
517
+ // Express answers as the router hands back, so a throw while answering,
518
+ // a head already written being the way, walks on to later error handlers
519
+ try {
520
+ router._sendOptionsReply(req, res, childMethods);
521
+ return this.resolve(true);
522
+ } catch (err) {
523
+ return this.step(err);
524
+ }
525
+ }
526
+ // An error carried out of the mount is not answered by the automatic reply, and
527
+ // stopping here handed it to the default page: express walks on to the error
528
+ // handlers written after the mount, for OPTIONS as for any other method.
529
+ this.step(undefined);
530
+ })
531
+ // a rejection out of the nested walk, or a throw above, must reject this one
532
+ // instead of dying as an unhandled rejection; wrapped for the native pair's
533
+ // receiver
534
+ .catch((err) => this.reject(err));
535
+ } else {
536
+ // errors and error handlers live out of line: this is the cold path, and its size
537
+ // was pushing step past the inlining threshold
538
+ if (req._error || kind === CALLBACK_ERROR) {
539
+ return this.errorHop(kind, callback);
540
+ }
541
+
542
+ try {
543
+ // handling OPTIONS method
544
+ if (req._isOptions && !route.all && route.method !== "OPTIONS") {
545
+ // an OPTIONS request always carries the set, see the Request constructor
546
+ const matched = /** @type {Set<string>} */ (req._matchedMethods);
547
+ matched.add(route.method);
548
+ if (route.gettable) {
549
+ matched.add("HEAD");
550
+ }
551
+ return this.step(undefined);
552
+ }
553
+ // entered only so its param callbacks could run, see the scan in dispatch: the verb
554
+ // cannot serve a HEAD, so nothing here answers it
555
+ if (req._isHead && !route.all && !route.gettable && route.method !== "HEAD") {
556
+ return this.step(undefined);
557
+ }
558
+
559
+ const out = callback(req, res, this.next);
560
+ if (out instanceof Promise) {
561
+ // Express 5 forwards a rejected handler promise to the error middleware on its
562
+ // own, so there is nothing for "catch async errors" or express-async-errors to
563
+ // opt into. A bare rejection carries no error, and express invents this one
564
+ out.catch((err) => {
565
+ req._error = err || new Error("Rejected promise");
566
+ req._errorKey = route.routeKey;
567
+ req._errorGroup = route.group;
568
+ return this.step(undefined);
569
+ });
570
+ }
571
+ } catch (err) {
572
+ req._error = err;
573
+ req._errorKey = route.routeKey;
574
+ req._errorGroup = route.group;
575
+ return this.step(undefined);
576
+ }
577
+ }
578
+ }
579
+ }
580
+
581
+ module.exports = Walk;