fulmine.js 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/package.json +1 -1
- package/src/application.js +109 -19
- package/src/cli.js +3 -3
- package/src/index.js +5 -0
- package/src/middlewares.js +87 -36
- package/src/node-shim.js +2 -1
- package/src/request.js +95 -37
- package/src/response.js +141 -77
- package/src/route.js +180 -0
- package/src/router.js +503 -124
- package/src/utils.js +41 -3
package/src/router.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
canBeOptimizedWithParams,
|
|
26
26
|
pathsCanOverlap,
|
|
27
27
|
uwsPrefersEarlier,
|
|
28
|
+
regexpGroupKeys,
|
|
28
29
|
NullObject,
|
|
29
30
|
EMPTY_REGEX
|
|
30
31
|
} = require("./utils.js");
|
|
@@ -77,6 +78,9 @@ class Walk {
|
|
|
77
78
|
this.skipUntil = skipUntil;
|
|
78
79
|
this.resolve = resolve;
|
|
79
80
|
this.reject = reject;
|
|
81
|
+
// read only by the native pair below, which has no promise to settle once for it; the
|
|
82
|
+
// promise path leaves it false. Initialized here to keep every walk the same shape
|
|
83
|
+
this.settled = false;
|
|
80
84
|
this.routeIndex = 0;
|
|
81
85
|
this.route = null;
|
|
82
86
|
this.callbackIndex = 0;
|
|
@@ -126,7 +130,7 @@ class Walk {
|
|
|
126
130
|
useApp(req, router);
|
|
127
131
|
// a chain that went into a mount never left it, since keepMount stops the pop, so the
|
|
128
132
|
// path is still relative to it. /alone/skip must not be offered to the app as /skip
|
|
129
|
-
if (req._stack.length > 0) {
|
|
133
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
130
134
|
req._stack.length = 0;
|
|
131
135
|
req._stackMounted = 0;
|
|
132
136
|
req.path = req._originalPath;
|
|
@@ -160,7 +164,9 @@ class Walk {
|
|
|
160
164
|
// the walk instead of becoming an unhandled rejection
|
|
161
165
|
Promise.resolve(continueRoute)
|
|
162
166
|
.then((resumed) => this.runRoute(resumed))
|
|
163
|
-
|
|
167
|
+
// wrapped so the native pair keeps the walk as receiver; a promise's reject
|
|
168
|
+
// would not have cared
|
|
169
|
+
.catch((err) => this.reject(err));
|
|
164
170
|
return;
|
|
165
171
|
}
|
|
166
172
|
return this.runRoute(continueRoute);
|
|
@@ -182,7 +188,7 @@ class Walk {
|
|
|
182
188
|
return false;
|
|
183
189
|
}
|
|
184
190
|
this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
|
|
185
|
-
if (req._stack.length > 0) {
|
|
191
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
186
192
|
req._stack.length = 0;
|
|
187
193
|
req._stackMounted = 0;
|
|
188
194
|
req.path = req._originalPath;
|
|
@@ -215,7 +221,7 @@ class Walk {
|
|
|
215
221
|
// Application, but the compiled mount route has no callback to do it
|
|
216
222
|
useApp(req, route.mountApp);
|
|
217
223
|
}
|
|
218
|
-
req._stack.push(route.regexMount ? regexMountEntry(router, route, req) : route.path);
|
|
224
|
+
(req._stack ??= []).push(route.regexMount ? regexMountEntry(router, route, req) : route.path);
|
|
219
225
|
// a use with no path consumes nothing, so everything below would work out the values
|
|
220
226
|
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
221
227
|
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
@@ -232,7 +238,7 @@ class Walk {
|
|
|
232
238
|
req.url = req._opPath + req.urlQuery;
|
|
233
239
|
req.path = req._opPath;
|
|
234
240
|
if (req._opPath === "") {
|
|
235
|
-
req.url = "/";
|
|
241
|
+
req.url = "/" + req.urlQuery;
|
|
236
242
|
req.path = "/";
|
|
237
243
|
}
|
|
238
244
|
req._lastUrl = req.url;
|
|
@@ -248,6 +254,32 @@ class Walk {
|
|
|
248
254
|
}
|
|
249
255
|
}
|
|
250
256
|
|
|
257
|
+
/**
|
|
258
|
+
* A hop while the request carries an error, or over an error handler it cannot run: the
|
|
259
|
+
* handler is invoked when the error is its to catch, everything else is skipped.
|
|
260
|
+
*
|
|
261
|
+
* @param {number} kind what the callback is, one of the CALLBACK_ constants
|
|
262
|
+
* @param {Function} callback
|
|
263
|
+
*/
|
|
264
|
+
errorHop(kind, callback) {
|
|
265
|
+
const req = this.req;
|
|
266
|
+
const route = this.route;
|
|
267
|
+
if (req._error && kind === CALLBACK_ERROR && route.routeKey >= req._errorKey) {
|
|
268
|
+
const out = this.router._handleError(req._error, callback, req, this.res);
|
|
269
|
+
if (out instanceof Promise) {
|
|
270
|
+
// an error handler's rejected promise moves on to the next error handler, and
|
|
271
|
+
// a bare rejection gets the error express invents for it
|
|
272
|
+
out.catch((err) => {
|
|
273
|
+
req._error = err || new Error("Rejected promise");
|
|
274
|
+
req._errorKey = route.routeKey;
|
|
275
|
+
return this.step(undefined);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
return this.step(undefined);
|
|
281
|
+
}
|
|
282
|
+
|
|
251
283
|
/**
|
|
252
284
|
* One hop, which is what next() does: with nothing, run the route's next callback; with "route",
|
|
253
285
|
* leave the route; with anything else, remember it as the error and carry on.
|
|
@@ -260,8 +292,13 @@ class Walk {
|
|
|
260
292
|
const route = this.route;
|
|
261
293
|
const router = this.router;
|
|
262
294
|
if (thingamabob) {
|
|
263
|
-
if (thingamabob === "route") {
|
|
295
|
+
if (thingamabob === "route" || thingamabob === "router") {
|
|
264
296
|
if (route.use && !route.keepMount) {
|
|
297
|
+
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
298
|
+
// req.url from the original path and would silently revert it
|
|
299
|
+
if (req.url !== req._lastUrl) {
|
|
300
|
+
req._absorbUrlRewrite();
|
|
301
|
+
}
|
|
265
302
|
if (req._stack.pop() !== "") {
|
|
266
303
|
req._stackMounted--;
|
|
267
304
|
}
|
|
@@ -280,7 +317,7 @@ class Walk {
|
|
|
280
317
|
req.url = req._opPath + req.urlQuery;
|
|
281
318
|
req.path = req._opPath;
|
|
282
319
|
if (req._opPath === "") {
|
|
283
|
-
req.url = "/";
|
|
320
|
+
req.url = "/" + req.urlQuery;
|
|
284
321
|
req.path = "/";
|
|
285
322
|
}
|
|
286
323
|
req._lastUrl = req.url;
|
|
@@ -296,6 +333,16 @@ class Walk {
|
|
|
296
333
|
useApp(req, req.app.parent);
|
|
297
334
|
}
|
|
298
335
|
}
|
|
336
|
+
if (thingamabob === "router") {
|
|
337
|
+
if (this.skipCheck) {
|
|
338
|
+
// on a compiled chain, leaving the router is what running out of chain
|
|
339
|
+
// already means: ordinary routing takes over after the mount
|
|
340
|
+
return this.dispatch(this.routes.length);
|
|
341
|
+
}
|
|
342
|
+
// out of this router entirely, so whoever mounted it carries on after the
|
|
343
|
+
// mount. The app's own walk has nobody after it, and answers 404
|
|
344
|
+
return this.resolve(false);
|
|
345
|
+
}
|
|
299
346
|
req.routeCount++;
|
|
300
347
|
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
301
348
|
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
@@ -326,8 +373,11 @@ class Walk {
|
|
|
326
373
|
}
|
|
327
374
|
const pushedParams = callback.settings.mergeParams;
|
|
328
375
|
if (pushedParams) {
|
|
329
|
-
req._paramStack.push(req.params);
|
|
376
|
+
(req._paramStack ??= []).push(req.params);
|
|
330
377
|
}
|
|
378
|
+
// express restores req.params when a router hands back, so what runs after the mount
|
|
379
|
+
// sees the params it had before it
|
|
380
|
+
const parentParams = req.params;
|
|
331
381
|
if (
|
|
332
382
|
callback.settings["strict routing"] &&
|
|
333
383
|
req.endsWithSlash &&
|
|
@@ -342,27 +392,36 @@ class Walk {
|
|
|
342
392
|
if (pushedParams) {
|
|
343
393
|
req._paramStack.pop();
|
|
344
394
|
}
|
|
395
|
+
req.params = parentParams;
|
|
345
396
|
if (req._error) {
|
|
346
397
|
req._errorKey = route.routeKey;
|
|
347
398
|
}
|
|
348
399
|
if (routed) return this.resolve(true);
|
|
349
400
|
if (req._isOptions && req._matchedMethods.size) {
|
|
350
|
-
// OPTIONS routing is different, it stops in the router if matched
|
|
401
|
+
// OPTIONS routing is different, it stops in the router if matched.
|
|
402
|
+
// Express answers as the router hands back, so a throw while answering,
|
|
403
|
+
// a head already written being the way, walks on to later error handlers
|
|
404
|
+
if (!req._error) {
|
|
405
|
+
try {
|
|
406
|
+
router._sendOptionsReply(req, res);
|
|
407
|
+
return this.resolve(true);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
return this.step(err);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
351
412
|
return this.resolve(false);
|
|
352
413
|
}
|
|
353
414
|
this.step(undefined);
|
|
354
415
|
})
|
|
355
416
|
// a rejection out of the nested walk, or a throw above, must reject this one
|
|
356
|
-
// instead of dying as an unhandled rejection
|
|
357
|
-
|
|
417
|
+
// instead of dying as an unhandled rejection; wrapped for the native pair's
|
|
418
|
+
// receiver
|
|
419
|
+
.catch((err) => this.reject(err));
|
|
358
420
|
} else {
|
|
359
|
-
//
|
|
421
|
+
// errors and error handlers live out of line: this is the cold path, and its size
|
|
422
|
+
// was pushing step past the inlining threshold
|
|
360
423
|
if (req._error || kind === CALLBACK_ERROR) {
|
|
361
|
-
|
|
362
|
-
return router._handleError(req._error, callback, req, res);
|
|
363
|
-
} else {
|
|
364
|
-
return this.step(undefined);
|
|
365
|
-
}
|
|
424
|
+
return this.errorHop(kind, callback);
|
|
366
425
|
}
|
|
367
426
|
|
|
368
427
|
try {
|
|
@@ -379,9 +438,10 @@ class Walk {
|
|
|
379
438
|
if (out instanceof Promise) {
|
|
380
439
|
// Express 5 forwards a rejected handler promise to the error middleware on its
|
|
381
440
|
// own, so there is nothing left for the "catch async errors" setting or for
|
|
382
|
-
// express-async-errors to opt into
|
|
441
|
+
// express-async-errors to opt into. A bare rejection carries no error, and
|
|
442
|
+
// express invents this one for it
|
|
383
443
|
out.catch((err) => {
|
|
384
|
-
req._error = err;
|
|
444
|
+
req._error = err || new Error("Rejected promise");
|
|
385
445
|
req._errorKey = route.routeKey;
|
|
386
446
|
return this.step(undefined);
|
|
387
447
|
});
|
|
@@ -395,6 +455,61 @@ class Walk {
|
|
|
395
455
|
}
|
|
396
456
|
}
|
|
397
457
|
|
|
458
|
+
/**
|
|
459
|
+
* The native handler's resolve, invoked as this.resolve(matched) with the walk as receiver. The
|
|
460
|
+
* promise pair _routeRequest allocates exists for callers that await; the uWS handler never did,
|
|
461
|
+
* and on the common path, where the handler answers and next() is never called, that promise
|
|
462
|
+
* never even settled: an async frame and two promises of floating garbage per request.
|
|
463
|
+
*
|
|
464
|
+
* The 404 epilogue stays on a microtask, exactly where the await used to resume: a middleware
|
|
465
|
+
* that writes after calling next() must still win the headersSent check, as it does in express.
|
|
466
|
+
* @this {Walk}
|
|
467
|
+
*/
|
|
468
|
+
function nativeDone(matched) {
|
|
469
|
+
if (this.settled) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.settled = true;
|
|
473
|
+
if (!matched) {
|
|
474
|
+
queueMicrotask(() => {
|
|
475
|
+
const response = this.res;
|
|
476
|
+
if (response.headersSent || response.aborted) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
this.router._endUnmatched(this.req, response);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (response.aborted || response.finished) {
|
|
483
|
+
console.error(err);
|
|
484
|
+
} else {
|
|
485
|
+
this.router._handleError(err, null, this.req, response);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The native handler's reject: answers 500 as express's final handler would, instead of dying as
|
|
494
|
+
* an unhandled rejection. Deferred like the resolve, since every rejection used to reach the
|
|
495
|
+
* handler's catch through an await.
|
|
496
|
+
* @this {Walk}
|
|
497
|
+
*/
|
|
498
|
+
function nativeFail(err) {
|
|
499
|
+
if (this.settled) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.settled = true;
|
|
503
|
+
queueMicrotask(() => {
|
|
504
|
+
const response = this.res;
|
|
505
|
+
if (response.aborted || response.finished) {
|
|
506
|
+
console.error(err);
|
|
507
|
+
} else {
|
|
508
|
+
this.router._handleError(err, null, this.req, response);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
398
513
|
/**
|
|
399
514
|
* What a RegExp mount consumed of the path, escaped so the mount-stack join can compile it as a
|
|
400
515
|
* literal. Exec runs on the same fixed-up path _pathMatches tested: a parent mount that consumed
|
|
@@ -414,6 +529,119 @@ function regexMountEntry(router, route, req) {
|
|
|
414
529
|
return matched ? escapePathLiteral(matched[0]) : "";
|
|
415
530
|
}
|
|
416
531
|
|
|
532
|
+
/**
|
|
533
|
+
* The route's own params merged with those of the mounts it sits under, in express's order: an
|
|
534
|
+
* outer mount first, the route's own last. Numbered captures do not overwrite each other, they
|
|
535
|
+
* shift, so a RegExp mount capturing one group leaves the route's own group numbered from one.
|
|
536
|
+
*
|
|
537
|
+
* @param {Record<string, any>} own what this route's own pattern captured
|
|
538
|
+
* @param {Record<string, any>[]} stack the mounts, outermost first
|
|
539
|
+
* @returns {Record<string, any>}
|
|
540
|
+
*/
|
|
541
|
+
function mergeParams(own, stack) {
|
|
542
|
+
const merged = Object.create(null);
|
|
543
|
+
for (const params of stack) {
|
|
544
|
+
Object.assign(merged, params);
|
|
545
|
+
}
|
|
546
|
+
// both sides numbering from zero means the outer ones keep their places and these move up
|
|
547
|
+
if (own[0] !== undefined && merged[0] !== undefined) {
|
|
548
|
+
let count = 0;
|
|
549
|
+
while (merged[count] !== undefined) {
|
|
550
|
+
count++;
|
|
551
|
+
}
|
|
552
|
+
let last = 0;
|
|
553
|
+
while (own[last] !== undefined) {
|
|
554
|
+
last++;
|
|
555
|
+
}
|
|
556
|
+
for (last--; last >= 0; last--) {
|
|
557
|
+
own[last + count] = own[last];
|
|
558
|
+
if (last < count) {
|
|
559
|
+
delete own[last];
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return Object.assign(merged, own);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The scheme and authority of an absolute request target, or "" for the ordinary kind.
|
|
568
|
+
*
|
|
569
|
+
* A request line may carry the whole URI, and express matches on the path while leaving req.url as
|
|
570
|
+
* it arrived. Same rule it uses: a "://" before any "?" means everything up to the slash after it
|
|
571
|
+
* is not path.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} url
|
|
574
|
+
* @returns {string}
|
|
575
|
+
*/
|
|
576
|
+
function protohostOf(url) {
|
|
577
|
+
if (url.length === 0 || url.charCodeAt(0) === 0x2f) {
|
|
578
|
+
return "";
|
|
579
|
+
}
|
|
580
|
+
const searchIndex = url.indexOf("?");
|
|
581
|
+
const pathLength = searchIndex === -1 ? url.length : searchIndex;
|
|
582
|
+
const fqdnIndex = url.slice(0, pathLength).indexOf("://");
|
|
583
|
+
if (fqdnIndex === -1) {
|
|
584
|
+
return "";
|
|
585
|
+
}
|
|
586
|
+
const slash = url.indexOf("/", fqdnIndex + 3);
|
|
587
|
+
return slash === -1 ? url : url.slice(0, slash);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Fills in what dispatch reads on a request that did not come from µWS.
|
|
592
|
+
*
|
|
593
|
+
* express's router can be driven with a plain object, `router.handle({ url, method }, res, next)`,
|
|
594
|
+
* and its own tests do exactly that; so does anything mounting a router on a server of its own.
|
|
595
|
+
* Only ever called for such a request: one of ours arrives with these fields already set.
|
|
596
|
+
*
|
|
597
|
+
* req.url becomes an accessor, so the router goes on writing plain paths to it while a reader sees
|
|
598
|
+
* the absolute URI it arrived as. That keeps the protohost out of the dispatch itself.
|
|
599
|
+
*
|
|
600
|
+
* @param {any} req
|
|
601
|
+
* @param {any} router
|
|
602
|
+
*/
|
|
603
|
+
function adoptPlainRequest(req, router) {
|
|
604
|
+
const arrived = typeof req.url === "string" ? req.url : "";
|
|
605
|
+
const protohost = protohostOf(arrived);
|
|
606
|
+
let raw = arrived.slice(protohost.length);
|
|
607
|
+
if (protohost !== "") {
|
|
608
|
+
Object.defineProperty(req, "url", {
|
|
609
|
+
configurable: true,
|
|
610
|
+
enumerable: true,
|
|
611
|
+
get() {
|
|
612
|
+
return protohost + raw;
|
|
613
|
+
},
|
|
614
|
+
set(value) {
|
|
615
|
+
const written = String(value);
|
|
616
|
+
raw = written.startsWith(protohost) ? written.slice(protohost.length) : written;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const queryIndex = raw.indexOf("?");
|
|
622
|
+
const path = queryIndex === -1 ? raw : raw.slice(0, queryIndex);
|
|
623
|
+
req.urlQuery = queryIndex === -1 ? "" : raw.slice(queryIndex);
|
|
624
|
+
req._rawQuery = req.urlQuery.slice(1);
|
|
625
|
+
req.path = path;
|
|
626
|
+
req.originalUrl = req.originalUrl ?? arrived;
|
|
627
|
+
req._originalPath = path;
|
|
628
|
+
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
629
|
+
req._opPath = req.endsWithSlash && path !== "/" && !router.get("strict routing") ? path.slice(0, -1) : path;
|
|
630
|
+
req._lastUrl = req.url;
|
|
631
|
+
req._isOptions = req.method === "OPTIONS";
|
|
632
|
+
req._isHead = req.method === "HEAD";
|
|
633
|
+
req.params = req.params ?? Object.create(null);
|
|
634
|
+
// null, not fresh arrays: the push sites materialize them on the first mount, and most
|
|
635
|
+
// requests never see one, same as the Request constructor
|
|
636
|
+
req._stack = null;
|
|
637
|
+
req._stackMounted = 0;
|
|
638
|
+
req._paramStack = null;
|
|
639
|
+
req._matchedMethods = req._isOptions ? new Set() : null;
|
|
640
|
+
req.routeCount = 1;
|
|
641
|
+
// read when a mount is left, and there is no application here to read it from
|
|
642
|
+
req.app = req.app ?? router;
|
|
643
|
+
}
|
|
644
|
+
|
|
417
645
|
/**
|
|
418
646
|
* Refuses a handler that could never be called, where it was written rather than on the first
|
|
419
647
|
* request that reaches it. Express words both of these and applications match on the text.
|
|
@@ -421,6 +649,31 @@ function regexMountEntry(router, route, req) {
|
|
|
421
649
|
* @param {any[]} handlers
|
|
422
650
|
* @param {string} [emptyMessage] app.use() says it its own way
|
|
423
651
|
*/
|
|
652
|
+
/**
|
|
653
|
+
* The uWS onAborted handler, bound to the response: a closure here captured two locals and cost
|
|
654
|
+
* a context plus a function per request, for a path that only ever runs on a client abort.
|
|
655
|
+
* @this {any} the response, with the request linked as this.req
|
|
656
|
+
*/
|
|
657
|
+
function onNativeAborted() {
|
|
658
|
+
const response = this;
|
|
659
|
+
const request = response.req;
|
|
660
|
+
// node's wording for a client abort, which is what body consumers match on
|
|
661
|
+
/** @type {NodeJS.ErrnoException} */
|
|
662
|
+
const err = new Error("aborted");
|
|
663
|
+
err.code = "ECONNRESET";
|
|
664
|
+
response.aborted = true;
|
|
665
|
+
response.finished = true;
|
|
666
|
+
// node's order on the request: 'aborted', then the stream dies, then 'close'. The
|
|
667
|
+
// error goes only to whoever listens for it, since a destroy(err) with no listener
|
|
668
|
+
// would take down the process
|
|
669
|
+
request.emit("aborted");
|
|
670
|
+
request.destroy(request.listenerCount("error") > 0 ? err : undefined);
|
|
671
|
+
response.socket?.emit("error", err);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
*
|
|
676
|
+
*/
|
|
424
677
|
function checkHandlers(handlers, emptyMessage = "argument handler is required") {
|
|
425
678
|
if (handlers.length === 0) {
|
|
426
679
|
throw new TypeError(emptyMessage);
|
|
@@ -449,6 +702,14 @@ function useApp(req, app) {
|
|
|
449
702
|
if (req.res) {
|
|
450
703
|
req.res.app = app;
|
|
451
704
|
}
|
|
705
|
+
// an app's own request/response extensions apply while it runs: express re-parents both
|
|
706
|
+
// objects on entering a mounted app, and this is the equivalent hop
|
|
707
|
+
if (app.request && Object.getPrototypeOf(req) !== app.request) {
|
|
708
|
+
Object.setPrototypeOf(req, app.request);
|
|
709
|
+
}
|
|
710
|
+
if (app.response && req.res && Object.getPrototypeOf(req.res) !== app.response) {
|
|
711
|
+
Object.setPrototypeOf(req.res, app.response);
|
|
712
|
+
}
|
|
452
713
|
}
|
|
453
714
|
|
|
454
715
|
// Every verb node knows about, which is the list the methods package hands Express, and "all" on
|
|
@@ -541,10 +802,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
541
802
|
/** @type {string|string[]} */
|
|
542
803
|
this.mountpath = "/";
|
|
543
804
|
this.settings = settings;
|
|
805
|
+
// the base classes; an Application replaces these with its own per-app subclasses, and a
|
|
806
|
+
// plain router has no request/response prototype layer, as in Express
|
|
544
807
|
this._request = Request;
|
|
545
808
|
this._response = Response;
|
|
546
|
-
this.request = this._request.prototype;
|
|
547
|
-
this.response = this._response.prototype;
|
|
548
809
|
|
|
549
810
|
if (typeof settings.caseSensitive !== "undefined") {
|
|
550
811
|
this.settings["case sensitive routing"] = settings.caseSensitive;
|
|
@@ -554,10 +815,6 @@ module.exports = class Router extends EventEmitter {
|
|
|
554
815
|
this.settings["strict routing"] = settings.strict;
|
|
555
816
|
delete this.settings.strict;
|
|
556
817
|
}
|
|
557
|
-
|
|
558
|
-
if (typeof this.settings["case sensitive routing"] === "undefined") {
|
|
559
|
-
this.settings["case sensitive routing"] = true;
|
|
560
|
-
}
|
|
561
818
|
}
|
|
562
819
|
|
|
563
820
|
/**
|
|
@@ -597,12 +854,35 @@ module.exports = class Router extends EventEmitter {
|
|
|
597
854
|
// req.app.get("view engine") inside a sub-app reads the sub-app's settings and not the
|
|
598
855
|
// settings of whatever handed the request over. A plain router is not an app and leaves it
|
|
599
856
|
// alone, which is what Express's router.handle does too.
|
|
857
|
+
// a plain object, which is how express's router can be driven and how its own tests drive
|
|
858
|
+
// it. One of ours arrives with these set, so this costs a property read
|
|
859
|
+
if (req._opPath === undefined) {
|
|
860
|
+
if (typeof req.url !== "string" || req.url === "") {
|
|
861
|
+
// express reads the path with parseurl, which answers nothing for these, and it
|
|
862
|
+
// hands the request straight back rather than running its pathless middleware
|
|
863
|
+
return next ? next() : undefined;
|
|
864
|
+
}
|
|
865
|
+
adoptPlainRequest(req, this);
|
|
866
|
+
}
|
|
600
867
|
if (this.constructor.name === "Application") {
|
|
601
868
|
useApp(req, this);
|
|
602
869
|
}
|
|
870
|
+
// express restores req.params when a router hands back, so the caller that ran this one
|
|
871
|
+
// sees the params it had before
|
|
872
|
+
const callerParams = req.params;
|
|
603
873
|
const routed = await this._routeRequest(req, res, 0);
|
|
604
|
-
if (!routed
|
|
605
|
-
|
|
874
|
+
if (!routed) {
|
|
875
|
+
req.params = callerParams;
|
|
876
|
+
if (next) {
|
|
877
|
+
// an error nobody handled belongs to the caller, as it does in express
|
|
878
|
+
const err = req._error;
|
|
879
|
+
if (err !== undefined) {
|
|
880
|
+
delete req._error;
|
|
881
|
+
delete req._errorKey;
|
|
882
|
+
return next(err);
|
|
883
|
+
}
|
|
884
|
+
next();
|
|
885
|
+
}
|
|
606
886
|
}
|
|
607
887
|
}
|
|
608
888
|
|
|
@@ -641,8 +921,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
641
921
|
getFullMountpath(req) {
|
|
642
922
|
// path-less app.use() pushes "", so a stack of only those joins to "" no matter how deep it is.
|
|
643
923
|
// patternToRegex("", true) is EMPTY_REGEX, so this returns exactly what the join path would,
|
|
644
|
-
// without walking the whole stack on every hop
|
|
645
|
-
|
|
924
|
+
// without walking the whole stack on every hop. _stackMounted first: it is 0 whenever
|
|
925
|
+
// _stack is still null, and req.baseUrl asks from unmounted requests too
|
|
926
|
+
if (req._stackMounted === 0 || req._stack.length === 0) {
|
|
646
927
|
return EMPTY_REGEX;
|
|
647
928
|
}
|
|
648
929
|
const fullStack = req._stack.join("");
|
|
@@ -654,9 +935,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
654
935
|
this._mountpathCache.clear();
|
|
655
936
|
}
|
|
656
937
|
// two mounts in the stack may reuse a name, which a named-group compile refuses.
|
|
657
|
-
// Nothing ever reads these groups, so they are renamed by position
|
|
658
|
-
|
|
659
|
-
|
|
938
|
+
// Nothing ever reads these groups, so they are renamed by position. An escaped colon
|
|
939
|
+
// is a literal one, out of a RegExp mount's matched text, and is left alone
|
|
940
|
+
const stackPattern = fullStack.includes(":")
|
|
941
|
+
? fullStack.replace(/(\\?):(\w+)/g, (whole, escaped, name, at) => (escaped ? whole : ":m" + at))
|
|
942
|
+
: fullStack;
|
|
943
|
+
fullMountpath = patternToRegex(stackPattern, true, Boolean(this.get("case sensitive routing")));
|
|
660
944
|
this._mountpathCache.set(fullStack, fullMountpath);
|
|
661
945
|
}
|
|
662
946
|
return fullMountpath;
|
|
@@ -675,7 +959,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
675
959
|
let path = req._opPath;
|
|
676
960
|
let pattern = route.pattern;
|
|
677
961
|
|
|
678
|
-
if (
|
|
962
|
+
if (route.userRegexp) {
|
|
963
|
+
// a RegExp the application wrote is matched against the path as it arrived: express
|
|
964
|
+
// relaxes a trailing slash only for the paths it compiled itself
|
|
965
|
+
if (req.endsWithSlash && !path.endsWith("/")) {
|
|
966
|
+
path += "/";
|
|
967
|
+
}
|
|
968
|
+
} else if (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
|
|
679
969
|
path = path.slice(0, -1);
|
|
680
970
|
}
|
|
681
971
|
// the line above turns the root path into the empty string, which no pattern is written
|
|
@@ -698,6 +988,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
698
988
|
if (pattern === EMPTY_REGEX) {
|
|
699
989
|
return true;
|
|
700
990
|
}
|
|
991
|
+
if (route.regexMount) {
|
|
992
|
+
// a mount consumes what it matched, so the match has to start the path and break on a
|
|
993
|
+
// separator: express refuses /api/ as a mount of /test/api/1234 for that reason
|
|
994
|
+
const matched = pattern.exec(path);
|
|
995
|
+
if (!matched || path.slice(0, matched[0].length) !== matched[0]) {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
const after = path[matched[0].length];
|
|
999
|
+
return after === undefined || after === "/";
|
|
1000
|
+
}
|
|
701
1001
|
return pattern.test(path);
|
|
702
1002
|
}
|
|
703
1003
|
|
|
@@ -720,7 +1020,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
720
1020
|
const paths = Array.isArray(path) ? path : [path];
|
|
721
1021
|
const routes = [];
|
|
722
1022
|
for (let path of paths) {
|
|
723
|
-
|
|
1023
|
+
// a mount always drops it, strict routing or not: strictness is about the end of a
|
|
1024
|
+
// path, and a mount has none. Express registers its use layers with strict off
|
|
1025
|
+
if (
|
|
1026
|
+
(method === "USE" || !this.get("strict routing")) &&
|
|
1027
|
+
typeof path === "string" &&
|
|
1028
|
+
path.endsWith("/") &&
|
|
1029
|
+
path !== "/"
|
|
1030
|
+
) {
|
|
724
1031
|
path = path.slice(0, -1);
|
|
725
1032
|
}
|
|
726
1033
|
if (path === "*") {
|
|
@@ -730,7 +1037,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
730
1037
|
method: method === "USE" ? "ALL" : method,
|
|
731
1038
|
path,
|
|
732
1039
|
pattern:
|
|
733
|
-
method === "USE" || needsConversionToRegex(path)
|
|
1040
|
+
method === "USE" || needsConversionToRegex(path)
|
|
1041
|
+
? // Boolean, not the raw value: an unset setting reads undefined, which a
|
|
1042
|
+
// default parameter would silently turn back into case-sensitive
|
|
1043
|
+
patternToRegex(path, method === "USE", Boolean(this.get("case sensitive routing")))
|
|
1044
|
+
: path,
|
|
734
1045
|
callbacks,
|
|
735
1046
|
// instanceof walks a prototype chain and length is a property load, and both used
|
|
736
1047
|
// to run for every callback of every hop
|
|
@@ -744,6 +1055,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
744
1055
|
// a mount written as a RegExp matches a piece of path that is not known until a
|
|
745
1056
|
// request comes in, so its stack entry cannot be the path itself
|
|
746
1057
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
1058
|
+
// written by the application, so express matches it as it stands
|
|
1059
|
+
userRegexp: path instanceof RegExp,
|
|
747
1060
|
routeKey: routeKey++,
|
|
748
1061
|
// the router this was registered on. Ordinary dispatch is done by that router, so
|
|
749
1062
|
// it could ask itself, but an optimized chain is walked by the app whatever it
|
|
@@ -758,9 +1071,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
758
1071
|
gettable: method === "GET" || method === "HEAD"
|
|
759
1072
|
};
|
|
760
1073
|
if (
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1074
|
+
route.pattern instanceof RegExp &&
|
|
1075
|
+
// a RegExp the application wrote: its capture groups are params too
|
|
1076
|
+
(path instanceof RegExp ||
|
|
1077
|
+
(typeof route.path === "string" &&
|
|
1078
|
+
(route.path.includes(":") || route.path.includes("*") || route.path.includes("{"))))
|
|
764
1079
|
) {
|
|
765
1080
|
route.complex = true;
|
|
766
1081
|
}
|
|
@@ -785,6 +1100,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
785
1100
|
// a route with a parameter matches paths its own text does not, so what an earlier route
|
|
786
1101
|
// could answer is compared shape against shape and not against that text
|
|
787
1102
|
const withParams = typeof route.path === "string" && route.path.includes(":");
|
|
1103
|
+
// under insensitive routing two paths that differ only in case answer the same requests,
|
|
1104
|
+
// so the text comparisons below run on the folded form. µWS itself still matches bytes:
|
|
1105
|
+
// a request in the registered case takes the chain, any other case takes the fallback,
|
|
1106
|
+
// and both answer as express would as long as the chain agrees with registration order
|
|
1107
|
+
const caseSensitive = Boolean(this.get("case sensitive routing"));
|
|
1108
|
+
const routePathFolded = caseSensitive || typeof route.path !== "string" ? route.path : route.path.toLowerCase();
|
|
788
1109
|
|
|
789
1110
|
for (let i = 0; i < routes.length; i++) {
|
|
790
1111
|
const r = routes[i];
|
|
@@ -802,11 +1123,38 @@ module.exports = class Router extends EventEmitter {
|
|
|
802
1123
|
}
|
|
803
1124
|
}
|
|
804
1125
|
|
|
1126
|
+
// a RegExp mount runs only where its match starts the path and breaks on a separator,
|
|
1127
|
+
// which is decidable here against a literal path and not against one with a parameter
|
|
1128
|
+
if (r.regexMount) {
|
|
1129
|
+
const matched = typeof route.path === "string" ? r.pattern.exec(route.path) : null;
|
|
1130
|
+
const runsAlways =
|
|
1131
|
+
matched !== null &&
|
|
1132
|
+
!matched[0].includes(":") &&
|
|
1133
|
+
route.path.slice(0, matched[0].length) === matched[0] &&
|
|
1134
|
+
(route.path.length === matched[0].length || route.path[matched[0].length] === "/");
|
|
1135
|
+
if (runsAlways) {
|
|
1136
|
+
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
optimizedPath.push(r);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
// it may still answer some of the paths this route matches, and the chain has no
|
|
1143
|
+
// way to say "only sometimes"
|
|
1144
|
+
if (matched !== null || withParams) {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
805
1150
|
// check if the paths match. A route with parameters is excluded from the text test:
|
|
806
1151
|
// its literal ":name" text would let an earlier regex in on requests it never matches
|
|
807
1152
|
if (
|
|
808
1153
|
(r.pattern instanceof RegExp && (!withParams || r.use) && r.pattern.test(route.path)) ||
|
|
809
|
-
(typeof r.pattern === "string" &&
|
|
1154
|
+
(typeof r.pattern === "string" &&
|
|
1155
|
+
(r.pattern === route.path ||
|
|
1156
|
+
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
1157
|
+
r.pattern === "/*"))
|
|
810
1158
|
) {
|
|
811
1159
|
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
812
1160
|
return false; // cant optimize nested routers with matches
|
|
@@ -822,15 +1170,17 @@ module.exports = class Router extends EventEmitter {
|
|
|
822
1170
|
if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
|
|
823
1171
|
return false;
|
|
824
1172
|
}
|
|
825
|
-
|
|
1173
|
+
const rPathFolded = caseSensitive ? r.path : r.path.toLowerCase();
|
|
1174
|
+
if (!pathsCanOverlap(rPathFolded, routePathFolded, r.use)) {
|
|
826
1175
|
continue;
|
|
827
1176
|
}
|
|
828
1177
|
if (r.use) {
|
|
829
1178
|
return false;
|
|
830
1179
|
}
|
|
831
1180
|
// the same path lands on the same µWS registration, so the earlier route runs first
|
|
832
|
-
// from inside the chain, under its own parameter names
|
|
833
|
-
|
|
1181
|
+
// from inside the chain, under its own parameter names; a case variant of it lands on
|
|
1182
|
+
// its own registration, whose chain was computed the same way, or on the fallback
|
|
1183
|
+
if (rPathFolded === routePathFolded) {
|
|
834
1184
|
if (r.callbacks.some((c) => c instanceof Router)) {
|
|
835
1185
|
return false;
|
|
836
1186
|
}
|
|
@@ -838,8 +1188,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
838
1188
|
continue;
|
|
839
1189
|
}
|
|
840
1190
|
// otherwise the two overlap only where µWS itself hands the request to the earlier,
|
|
841
|
-
// more specific registration, so this chain never sees those paths
|
|
842
|
-
|
|
1191
|
+
// more specific registration, so this chain never sees those paths. That argument is
|
|
1192
|
+
// about bytes, so under insensitive routing it only holds when no literal hides
|
|
1193
|
+
// behind a case difference
|
|
1194
|
+
if (
|
|
1195
|
+
!r.optimizedPath ||
|
|
1196
|
+
!uwsPrefersEarlier(r.path, route.path) ||
|
|
1197
|
+
(!caseSensitive && (r.path !== rPathFolded || route.path !== routePathFolded))
|
|
1198
|
+
) {
|
|
843
1199
|
return false;
|
|
844
1200
|
}
|
|
845
1201
|
}
|
|
@@ -854,7 +1210,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
854
1210
|
* needs every route to have been registered first.
|
|
855
1211
|
*/
|
|
856
1212
|
_compileOptimizedRoutes() {
|
|
857
|
-
if (!this.uwsApp
|
|
1213
|
+
if (!this.uwsApp) {
|
|
858
1214
|
return;
|
|
859
1215
|
}
|
|
860
1216
|
|
|
@@ -862,15 +1218,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
862
1218
|
const walk = (router, pathPrefix, chainPrefix) => {
|
|
863
1219
|
for (const route of router._routes) {
|
|
864
1220
|
if (route.use) {
|
|
865
|
-
// only sole-callback mounts
|
|
866
|
-
//
|
|
1221
|
+
// only sole-callback mounts. Case rules do not gate the walk: each level's
|
|
1222
|
+
// _optimizeRoute guards its own routes under its own setting, and a request
|
|
1223
|
+
// in any other case than the registered one takes the fallback, which
|
|
1224
|
+
// honours the child's setting on its own
|
|
867
1225
|
if (
|
|
868
1226
|
!route.complex &&
|
|
869
1227
|
canBeOptimized(route.path) &&
|
|
870
1228
|
route.path !== "/*" &&
|
|
871
1229
|
route.callbacks.length === 1 &&
|
|
872
|
-
route.callbacks[0] instanceof Router
|
|
873
|
-
route.callbacks[0].get("case sensitive routing")
|
|
1230
|
+
route.callbacks[0] instanceof Router
|
|
874
1231
|
) {
|
|
875
1232
|
let pathToMount = router._optimizeRoute(route, router._routes);
|
|
876
1233
|
if (!pathToMount) {
|
|
@@ -921,6 +1278,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
921
1278
|
continue;
|
|
922
1279
|
}
|
|
923
1280
|
}
|
|
1281
|
+
// the prefix goes in whether or not the mount had a path: a pathless mount
|
|
1282
|
+
// adds nothing to the path and everything to the chain, the middlewares in
|
|
1283
|
+
// front of it and the mount entry that says where to resume
|
|
1284
|
+
const chain = chainPrefix.length > 0 ? [...chainPrefix, ...leafPath] : leafPath;
|
|
924
1285
|
if (pathPrefix) {
|
|
925
1286
|
const registered = {
|
|
926
1287
|
...route,
|
|
@@ -928,7 +1289,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
928
1289
|
pattern: pathPrefix + route.path,
|
|
929
1290
|
optimizedRouter: true
|
|
930
1291
|
};
|
|
931
|
-
this._registerUwsRoute(registered,
|
|
1292
|
+
this._registerUwsRoute(registered, chain);
|
|
932
1293
|
// the chain holds the original object, so the request-time guard has to
|
|
933
1294
|
// find the computed fields there, or a mounted param route extracts its
|
|
934
1295
|
// params twice. The names match: the prefix is static, so the copy's path
|
|
@@ -936,7 +1297,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
936
1297
|
route.optimizedParams = registered.optimizedParams;
|
|
937
1298
|
route.optimizedPath = registered.optimizedPath;
|
|
938
1299
|
} else {
|
|
939
|
-
this._registerUwsRoute(route,
|
|
1300
|
+
this._registerUwsRoute(route, chain);
|
|
940
1301
|
}
|
|
941
1302
|
}
|
|
942
1303
|
}
|
|
@@ -947,33 +1308,21 @@ module.exports = class Router extends EventEmitter {
|
|
|
947
1308
|
|
|
948
1309
|
/**
|
|
949
1310
|
* Wraps a uWS request and response in ours and links them, which is the first thing every
|
|
950
|
-
* request does whichever path serves it.
|
|
1311
|
+
* request does whichever path serves it. The response rides back as request.res: returning
|
|
1312
|
+
* a `{ request, response }` pair was one throwaway object per request.
|
|
951
1313
|
*
|
|
952
1314
|
* @param {any} res uWS response
|
|
953
1315
|
* @param {any} req uWS request, readable only during this call
|
|
954
|
-
* @returns {
|
|
1316
|
+
* @returns {any} the request, with the response reachable as request.res
|
|
955
1317
|
*/
|
|
956
1318
|
handleRequest(res, req) {
|
|
957
1319
|
const request = new this._request(req, res, this);
|
|
958
1320
|
const response = new this._response(res, request, this);
|
|
959
1321
|
request.res = response;
|
|
960
1322
|
response.req = request;
|
|
961
|
-
res.onAborted(()
|
|
962
|
-
// node's wording for a client abort, which is what body consumers match on
|
|
963
|
-
/** @type {NodeJS.ErrnoException} */
|
|
964
|
-
const err = new Error("aborted");
|
|
965
|
-
err.code = "ECONNRESET";
|
|
966
|
-
response.aborted = true;
|
|
967
|
-
response.finished = true;
|
|
968
|
-
// node's order on the request: 'aborted', then the stream dies, then 'close'. The
|
|
969
|
-
// error goes only to whoever listens for it, since a destroy(err) with no listener
|
|
970
|
-
// would take down the process
|
|
971
|
-
request.emit("aborted");
|
|
972
|
-
request.destroy(request.listenerCount("error") > 0 ? err : undefined);
|
|
973
|
-
response.socket?.emit("error", err);
|
|
974
|
-
});
|
|
1323
|
+
res.onAborted(onNativeAborted.bind(response));
|
|
975
1324
|
|
|
976
|
-
return
|
|
1325
|
+
return request;
|
|
977
1326
|
}
|
|
978
1327
|
|
|
979
1328
|
/**
|
|
@@ -989,6 +1338,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
989
1338
|
* @returns {boolean}
|
|
990
1339
|
*/
|
|
991
1340
|
_isFollowedByAnOverlap(route, routes) {
|
|
1341
|
+
// folded under insensitive routing, where a case variant answers the same requests
|
|
1342
|
+
const caseSensitive = Boolean(this.get("case sensitive routing"));
|
|
1343
|
+
const routePath = caseSensitive ? route.path : route.path.toLowerCase();
|
|
992
1344
|
for (let i = routes.length - 1; i >= 0; i--) {
|
|
993
1345
|
const later = routes[i];
|
|
994
1346
|
if (later.routeKey <= route.routeKey) {
|
|
@@ -1002,7 +1354,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1002
1354
|
return true;
|
|
1003
1355
|
}
|
|
1004
1356
|
if (typeof later.path === "string" && canBeOptimizedWithParams(later.path)) {
|
|
1005
|
-
if (pathsCanOverlap(
|
|
1357
|
+
if (pathsCanOverlap(routePath, caseSensitive ? later.path : later.path.toLowerCase())) {
|
|
1006
1358
|
return true;
|
|
1007
1359
|
}
|
|
1008
1360
|
continue;
|
|
@@ -1030,34 +1382,32 @@ module.exports = class Router extends EventEmitter {
|
|
|
1030
1382
|
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
1031
1383
|
}
|
|
1032
1384
|
const makeHandler = (chain) => {
|
|
1033
|
-
//
|
|
1034
|
-
// and a scan of the chain on every native request.
|
|
1385
|
+
// all three are registration-time constants: computing them in the handler was a
|
|
1386
|
+
// closure and a scan of the chain on every native request.
|
|
1035
1387
|
// Falling back resumes after the mount, not after the router's leaf: the leaf can have
|
|
1036
1388
|
// a lower routeKey than the parent's middlewares, and an error handler declared before
|
|
1037
1389
|
// the mount must not catch what the router threw
|
|
1038
1390
|
const mount = chain.find((r) => r.keepMount);
|
|
1039
1391
|
const skipUntil = mount ?? chain[chain.length - 1];
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1392
|
+
const optimizedParams = route.optimizedParams;
|
|
1393
|
+
// not async, and no _routeRequest: its promise pair exists for callers that await,
|
|
1394
|
+
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
1395
|
+
// microtask, which is where the await used to resume, so the visible order holds
|
|
1396
|
+
return (res, req) => {
|
|
1397
|
+
const request = this.handleRequest(res, req);
|
|
1398
|
+
const response = request.res;
|
|
1399
|
+
if (optimizedParams) {
|
|
1043
1400
|
request.optimizedParams = new NullObject();
|
|
1044
|
-
for (let i = 0; i <
|
|
1045
|
-
request.optimizedParams[
|
|
1401
|
+
for (let i = 0; i < optimizedParams.length; i++) {
|
|
1402
|
+
request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
|
|
1046
1403
|
}
|
|
1047
1404
|
}
|
|
1405
|
+
const walk = new Walk(this, request, response, chain, true, skipUntil, nativeDone, nativeFail);
|
|
1048
1406
|
try {
|
|
1049
|
-
|
|
1050
|
-
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
1051
|
-
this._endUnmatched(request, response);
|
|
1052
|
-
}
|
|
1407
|
+
walk.dispatch(0);
|
|
1053
1408
|
} catch (err) {
|
|
1054
|
-
//
|
|
1055
|
-
|
|
1056
|
-
if (response.aborted || response.finished) {
|
|
1057
|
-
console.error(err);
|
|
1058
|
-
} else {
|
|
1059
|
-
this._handleError(err, null, request, response);
|
|
1060
|
-
}
|
|
1409
|
+
// what a throw inside a promise executor did: reject, once
|
|
1410
|
+
nativeFail.call(walk, err);
|
|
1061
1411
|
}
|
|
1062
1412
|
};
|
|
1063
1413
|
};
|
|
@@ -1072,13 +1422,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1072
1422
|
const realFn = fn;
|
|
1073
1423
|
const headFn = getChain.length === optimizedPath.length ? realFn : makeHandler(optimizedPath);
|
|
1074
1424
|
|
|
1425
|
+
// the response prototype the route will really run under: its own app's, which sees a
|
|
1426
|
+
// method patched there or inherited from a parent app, falling back to the registering app
|
|
1427
|
+
const responseProto = /** @type {any} */ (route.owner)?.response ?? /** @type {any} */ (this).response;
|
|
1075
1428
|
// check if route is declarative
|
|
1076
1429
|
if (
|
|
1077
1430
|
optimizedPath.length === 1 && // must not have middlewares
|
|
1078
1431
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
1079
1432
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
1080
1433
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
1081
|
-
!resDecMethods.some((method) => resCodes[method] !==
|
|
1434
|
+
!resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
|
|
1082
1435
|
this.get("declarative responses") // must have declarative responses enabled
|
|
1083
1436
|
) {
|
|
1084
1437
|
const decRes = compileDeclarative(route.callbacks[0], this);
|
|
@@ -1116,11 +1469,18 @@ module.exports = class Router extends EventEmitter {
|
|
|
1116
1469
|
*/
|
|
1117
1470
|
_handleError(err, handler, request, response) {
|
|
1118
1471
|
if (handler) {
|
|
1119
|
-
|
|
1472
|
+
const next = (pass) => {
|
|
1120
1473
|
delete request._error;
|
|
1121
1474
|
delete request._errorKey;
|
|
1122
1475
|
return request.next(pass);
|
|
1123
|
-
}
|
|
1476
|
+
};
|
|
1477
|
+
try {
|
|
1478
|
+
return handler(err, request, response, next);
|
|
1479
|
+
} catch (thrown) {
|
|
1480
|
+
// what an error handler throws is the error the next one gets
|
|
1481
|
+
request._error = thrown;
|
|
1482
|
+
return request.next(thrown);
|
|
1483
|
+
}
|
|
1124
1484
|
}
|
|
1125
1485
|
console.error(err);
|
|
1126
1486
|
if (response.statusCode === 200) {
|
|
@@ -1165,23 +1525,30 @@ module.exports = class Router extends EventEmitter {
|
|
|
1165
1525
|
// Object.create(null) rather than the { __proto__: null } literal, which is the same object
|
|
1166
1526
|
// for 9ns more. Null-prototyped either way, as Express 5 makes params.
|
|
1167
1527
|
const obj = Object.create(null);
|
|
1168
|
-
if (!match
|
|
1528
|
+
if (!match) {
|
|
1169
1529
|
return obj;
|
|
1170
1530
|
}
|
|
1171
1531
|
|
|
1172
|
-
const groups = match.groups;
|
|
1173
1532
|
const meta = getPatternMeta(pattern);
|
|
1174
1533
|
if (meta === undefined) {
|
|
1175
|
-
// a RegExp the application supplied itself, which was never compiled here
|
|
1176
|
-
|
|
1177
|
-
|
|
1534
|
+
// a RegExp the application supplied itself, which was never compiled here: every
|
|
1535
|
+
// capture group lands in params, named ones under their name and the rest under their
|
|
1536
|
+
// position, which is what express does with one
|
|
1537
|
+
const keys = regexpGroupKeys(pattern);
|
|
1538
|
+
for (let i = 1; i < match.length; i++) {
|
|
1539
|
+
const value = match[i];
|
|
1178
1540
|
if (value === undefined) {
|
|
1179
1541
|
continue;
|
|
1180
1542
|
}
|
|
1181
|
-
obj[
|
|
1543
|
+
obj[keys[i - 1]] = decodeParam(value);
|
|
1182
1544
|
}
|
|
1183
1545
|
return obj;
|
|
1184
1546
|
}
|
|
1547
|
+
if (!match.groups) {
|
|
1548
|
+
return obj;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const groups = match.groups;
|
|
1185
1552
|
|
|
1186
1553
|
// asking for each name in turn rather than walking the groups object, which is a
|
|
1187
1554
|
// null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
|
|
@@ -1229,7 +1596,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1229
1596
|
}
|
|
1230
1597
|
} else if (route.complex) {
|
|
1231
1598
|
let path = req._originalPath;
|
|
1232
|
-
if (req._stack.length > 0) {
|
|
1599
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
1233
1600
|
const fullMountpath = this.getFullMountpath(req);
|
|
1234
1601
|
if (fullMountpath !== EMPTY_REGEX) {
|
|
1235
1602
|
path = path.replace(fullMountpath, "");
|
|
@@ -1245,29 +1612,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1245
1612
|
req._errorKey = route.routeKey;
|
|
1246
1613
|
return "route";
|
|
1247
1614
|
}
|
|
1248
|
-
if (req._paramStack.length > 0) {
|
|
1249
|
-
|
|
1250
|
-
// route's own extraction wins over all of them
|
|
1251
|
-
const own = req.params;
|
|
1252
|
-
const merged = Object.create(null);
|
|
1253
|
-
for (const params of req._paramStack) {
|
|
1254
|
-
Object.assign(merged, params);
|
|
1255
|
-
}
|
|
1256
|
-
req.params = Object.assign(merged, own);
|
|
1615
|
+
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
1616
|
+
req.params = mergeParams(req.params, req._paramStack);
|
|
1257
1617
|
}
|
|
1258
1618
|
} else {
|
|
1259
1619
|
// express 5 gives every matched route null-prototype params; only a pathless
|
|
1260
1620
|
// middleware layer keeps the plain object, as its router hands one to fast_slash
|
|
1261
1621
|
req.params = route.use && route.path === "" ? {} : Object.create(null);
|
|
1262
|
-
if (req._paramStack.length > 0) {
|
|
1263
|
-
|
|
1264
|
-
// route's own extraction wins over all of them
|
|
1265
|
-
const own = req.params;
|
|
1266
|
-
const merged = Object.create(null);
|
|
1267
|
-
for (const params of req._paramStack) {
|
|
1268
|
-
Object.assign(merged, params);
|
|
1269
|
-
}
|
|
1270
|
-
req.params = Object.assign(merged, own);
|
|
1622
|
+
if (req._paramStack !== null && req._paramStack.length > 0) {
|
|
1623
|
+
req.params = mergeParams(req.params, req._paramStack);
|
|
1271
1624
|
}
|
|
1272
1625
|
}
|
|
1273
1626
|
|
|
@@ -1389,6 +1742,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1389
1742
|
if (typeof name !== "string" && !Array.isArray(name)) {
|
|
1390
1743
|
throw new TypeError("argument name must be a string");
|
|
1391
1744
|
}
|
|
1745
|
+
if (fn === undefined) {
|
|
1746
|
+
throw new TypeError("argument fn is required");
|
|
1747
|
+
}
|
|
1748
|
+
if (typeof fn !== "function") {
|
|
1749
|
+
throw new TypeError("argument fn must be a function");
|
|
1750
|
+
}
|
|
1392
1751
|
const names = Array.isArray(name) ? name : [name];
|
|
1393
1752
|
for (const key of names) {
|
|
1394
1753
|
if (!this._paramCallbacks.has(key)) {
|
|
@@ -1490,6 +1849,29 @@ module.exports = class Router extends EventEmitter {
|
|
|
1490
1849
|
response.send(err);
|
|
1491
1850
|
}
|
|
1492
1851
|
|
|
1852
|
+
/**
|
|
1853
|
+
* The automatic OPTIONS reply, built from the methods the walk collected. Throws instead of
|
|
1854
|
+
* answering when the head has already been written, which is what node's setHeader would do
|
|
1855
|
+
* and what lets an error handler see it, as in Express.
|
|
1856
|
+
*
|
|
1857
|
+
* @param {any} request
|
|
1858
|
+
* @param {any} response
|
|
1859
|
+
*/
|
|
1860
|
+
_sendOptionsReply(request, response) {
|
|
1861
|
+
if (response._headWritten) {
|
|
1862
|
+
throw new Error("Cannot set headers after they are sent to the client");
|
|
1863
|
+
}
|
|
1864
|
+
// Express 5 sorts the methods and joins them with ", ", so the header reads the same
|
|
1865
|
+
// regardless of the order the routes happened to be registered in
|
|
1866
|
+
const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
|
|
1867
|
+
response.setHeader("Allow", allowedMethods);
|
|
1868
|
+
// the router package answers this one itself, with a plain-text body, the nosniff
|
|
1869
|
+
// header and end() rather than send(), so no ETag comes with it
|
|
1870
|
+
response.setHeader("Content-Type", "text/plain");
|
|
1871
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1872
|
+
response.end(allowedMethods);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1493
1875
|
/**
|
|
1494
1876
|
* How a request that nothing answered ends: with the error it carries, with the automatic
|
|
1495
1877
|
* OPTIONS reply, or with a 404. The native chain, the app's catch-all handler and the node shim
|
|
@@ -1503,15 +1885,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1503
1885
|
return this._handleError(request._error, null, request, response);
|
|
1504
1886
|
}
|
|
1505
1887
|
if (request._isOptions && request._matchedMethods.size > 0) {
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
response.setHeader("Content-Type", "text/plain");
|
|
1513
|
-
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1514
|
-
response.end(allowedMethods);
|
|
1888
|
+
try {
|
|
1889
|
+
this._sendOptionsReply(request, response);
|
|
1890
|
+
} catch (err) {
|
|
1891
|
+
// a head already written: the error answers instead, as express's does
|
|
1892
|
+
this._handleError(err, null, request, response);
|
|
1893
|
+
}
|
|
1515
1894
|
return;
|
|
1516
1895
|
}
|
|
1517
1896
|
response.status(404);
|