fulmine.js 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- .catch(this.reject);
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
- .catch(this.reject);
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
- // handle errors and error handlers
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
- if (req._error && kind === CALLBACK_ERROR && route.routeKey >= req._errorKey) {
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 && next) {
605
- next();
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
- if (!req._stack.length || req._stackMounted === 0) {
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
- const stackPattern = fullStack.includes(":") ? fullStack.replace(/:\w+/g, (m, at) => ":m" + at) : fullStack;
659
- fullMountpath = patternToRegex(stackPattern, true);
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 (req.endsWithSlash && path.endsWith("/") && !this.get("strict routing")) {
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
- if (!this.get("strict routing") && typeof path === "string" && path.endsWith("/") && path !== "/") {
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) ? patternToRegex(path, method === "USE") : 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
- typeof route.path === "string" &&
762
- (route.path.includes(":") || route.path.includes("*") || route.path.includes("{")) &&
763
- route.pattern instanceof RegExp
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" && (r.pattern === route.path || r.pattern === "/*"))
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
- if (!pathsCanOverlap(r.path, route.path, r.use)) {
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
- if (r.path === route.path) {
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
- if (!r.optimizedPath || !uwsPrefersEarlier(r.path, route.path)) {
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 || !this.get("case sensitive routing")) {
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, and only into routers that match with the same
866
- // case rules as µWS: the Walk fallback honours the child's setting, µWS cannot
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, [...chainPrefix, ...leafPath]);
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, leafPath);
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 {{request: any, response: any}}
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 { request, response };
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(route.path, later.path)) {
1357
+ if (pathsCanOverlap(routePath, caseSensitive ? later.path : later.path.toLowerCase())) {
1006
1358
  return true;
1007
1359
  }
1008
1360
  continue;
@@ -1030,34 +1382,36 @@ 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
- // both are registration-time constants: computing them in the handler was a closure
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
- return async (res, req) => {
1041
- const { request, response } = this.handleRequest(res, req);
1042
- if (route.optimizedParams) {
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 < route.optimizedParams.length; i++) {
1045
- request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
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
- const matchedRoute = await this._routeRequest(request, response, 0, chain, true, skipUntil);
1050
- if (!matchedRoute && !response.headersSent && !response.aborted) {
1051
- this._endUnmatched(request, response);
1052
- }
1407
+ walk.dispatch(0);
1053
1408
  } catch (err) {
1054
- // an internal throw answers 500 as express's final handler would, instead of
1055
- // dying as an unhandled rejection
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);
1411
+ } finally {
1412
+ // whatever runs after this line is outside the cork uWS held for this
1413
+ // callback, so later writes have to open their own
1414
+ response._corkNeeded = true;
1061
1415
  }
1062
1416
  };
1063
1417
  };
@@ -1072,13 +1426,16 @@ module.exports = class Router extends EventEmitter {
1072
1426
  const realFn = fn;
1073
1427
  const headFn = getChain.length === optimizedPath.length ? realFn : makeHandler(optimizedPath);
1074
1428
 
1429
+ // the response prototype the route will really run under: its own app's, which sees a
1430
+ // method patched there or inherited from a parent app, falling back to the registering app
1431
+ const responseProto = /** @type {any} */ (route.owner)?.response ?? /** @type {any} */ (this).response;
1075
1432
  // check if route is declarative
1076
1433
  if (
1077
1434
  optimizedPath.length === 1 && // must not have middlewares
1078
1435
  route.callbacks.length === 1 && // must not have multiple callbacks
1079
1436
  typeof route.callbacks[0] === "function" && // must be a function
1080
1437
  route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
1081
- !resDecMethods.some((method) => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
1438
+ !resDecMethods.some((method) => resCodes[method] !== responseProto[method].toString()) && // must not have injected methods
1082
1439
  this.get("declarative responses") // must have declarative responses enabled
1083
1440
  ) {
1084
1441
  const decRes = compileDeclarative(route.callbacks[0], this);
@@ -1116,11 +1473,18 @@ module.exports = class Router extends EventEmitter {
1116
1473
  */
1117
1474
  _handleError(err, handler, request, response) {
1118
1475
  if (handler) {
1119
- return handler(err, request, response, (pass) => {
1476
+ const next = (pass) => {
1120
1477
  delete request._error;
1121
1478
  delete request._errorKey;
1122
1479
  return request.next(pass);
1123
- });
1480
+ };
1481
+ try {
1482
+ return handler(err, request, response, next);
1483
+ } catch (thrown) {
1484
+ // what an error handler throws is the error the next one gets
1485
+ request._error = thrown;
1486
+ return request.next(thrown);
1487
+ }
1124
1488
  }
1125
1489
  console.error(err);
1126
1490
  if (response.statusCode === 200) {
@@ -1165,23 +1529,30 @@ module.exports = class Router extends EventEmitter {
1165
1529
  // Object.create(null) rather than the { __proto__: null } literal, which is the same object
1166
1530
  // for 9ns more. Null-prototyped either way, as Express 5 makes params.
1167
1531
  const obj = Object.create(null);
1168
- if (!match?.groups) {
1532
+ if (!match) {
1169
1533
  return obj;
1170
1534
  }
1171
1535
 
1172
- const groups = match.groups;
1173
1536
  const meta = getPatternMeta(pattern);
1174
1537
  if (meta === undefined) {
1175
- // a RegExp the application supplied itself, which was never compiled here
1176
- for (const name in groups) {
1177
- const value = groups[name];
1538
+ // a RegExp the application supplied itself, which was never compiled here: every
1539
+ // capture group lands in params, named ones under their name and the rest under their
1540
+ // position, which is what express does with one
1541
+ const keys = regexpGroupKeys(pattern);
1542
+ for (let i = 1; i < match.length; i++) {
1543
+ const value = match[i];
1178
1544
  if (value === undefined) {
1179
1545
  continue;
1180
1546
  }
1181
- obj[name] = decodeParam(value);
1547
+ obj[keys[i - 1]] = decodeParam(value);
1182
1548
  }
1183
1549
  return obj;
1184
1550
  }
1551
+ if (!match.groups) {
1552
+ return obj;
1553
+ }
1554
+
1555
+ const groups = match.groups;
1185
1556
 
1186
1557
  // asking for each name in turn rather than walking the groups object, which is a
1187
1558
  // null-prototype dictionary and slow to enumerate, and reading the wildcard answer that was
@@ -1229,7 +1600,7 @@ module.exports = class Router extends EventEmitter {
1229
1600
  }
1230
1601
  } else if (route.complex) {
1231
1602
  let path = req._originalPath;
1232
- if (req._stack.length > 0) {
1603
+ if (req._stack !== null && req._stack.length > 0) {
1233
1604
  const fullMountpath = this.getFullMountpath(req);
1234
1605
  if (fullMountpath !== EMPTY_REGEX) {
1235
1606
  path = path.replace(fullMountpath, "");
@@ -1245,29 +1616,15 @@ module.exports = class Router extends EventEmitter {
1245
1616
  req._errorKey = route.routeKey;
1246
1617
  return "route";
1247
1618
  }
1248
- if (req._paramStack.length > 0) {
1249
- // express's mergeParams order: later mounts override earlier ones, and the
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);
1619
+ if (req._paramStack !== null && req._paramStack.length > 0) {
1620
+ req.params = mergeParams(req.params, req._paramStack);
1257
1621
  }
1258
1622
  } else {
1259
1623
  // express 5 gives every matched route null-prototype params; only a pathless
1260
1624
  // middleware layer keeps the plain object, as its router hands one to fast_slash
1261
1625
  req.params = route.use && route.path === "" ? {} : Object.create(null);
1262
- if (req._paramStack.length > 0) {
1263
- // express's mergeParams order: later mounts override earlier ones, and the
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);
1626
+ if (req._paramStack !== null && req._paramStack.length > 0) {
1627
+ req.params = mergeParams(req.params, req._paramStack);
1271
1628
  }
1272
1629
  }
1273
1630
 
@@ -1389,6 +1746,12 @@ module.exports = class Router extends EventEmitter {
1389
1746
  if (typeof name !== "string" && !Array.isArray(name)) {
1390
1747
  throw new TypeError("argument name must be a string");
1391
1748
  }
1749
+ if (fn === undefined) {
1750
+ throw new TypeError("argument fn is required");
1751
+ }
1752
+ if (typeof fn !== "function") {
1753
+ throw new TypeError("argument fn must be a function");
1754
+ }
1392
1755
  const names = Array.isArray(name) ? name : [name];
1393
1756
  for (const key of names) {
1394
1757
  if (!this._paramCallbacks.has(key)) {
@@ -1490,6 +1853,29 @@ module.exports = class Router extends EventEmitter {
1490
1853
  response.send(err);
1491
1854
  }
1492
1855
 
1856
+ /**
1857
+ * The automatic OPTIONS reply, built from the methods the walk collected. Throws instead of
1858
+ * answering when the head has already been written, which is what node's setHeader would do
1859
+ * and what lets an error handler see it, as in Express.
1860
+ *
1861
+ * @param {any} request
1862
+ * @param {any} response
1863
+ */
1864
+ _sendOptionsReply(request, response) {
1865
+ if (response._headWritten) {
1866
+ throw new Error("Cannot set headers after they are sent to the client");
1867
+ }
1868
+ // Express 5 sorts the methods and joins them with ", ", so the header reads the same
1869
+ // regardless of the order the routes happened to be registered in
1870
+ const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
1871
+ response.setHeader("Allow", allowedMethods);
1872
+ // the router package answers this one itself, with a plain-text body, the nosniff
1873
+ // header and end() rather than send(), so no ETag comes with it
1874
+ response.setHeader("Content-Type", "text/plain");
1875
+ response.setHeader("X-Content-Type-Options", "nosniff");
1876
+ response.end(allowedMethods);
1877
+ }
1878
+
1493
1879
  /**
1494
1880
  * How a request that nothing answered ends: with the error it carries, with the automatic
1495
1881
  * OPTIONS reply, or with a 404. The native chain, the app's catch-all handler and the node shim
@@ -1503,15 +1889,12 @@ module.exports = class Router extends EventEmitter {
1503
1889
  return this._handleError(request._error, null, request, response);
1504
1890
  }
1505
1891
  if (request._isOptions && request._matchedMethods.size > 0) {
1506
- // Express 5 sorts the methods and joins them with ", ", so the header reads the same
1507
- // regardless of the order the routes happened to be registered in
1508
- const allowedMethods = Array.from(request._matchedMethods).sort().join(", ");
1509
- response.setHeader("Allow", allowedMethods);
1510
- // the router package answers this one itself, with a plain-text body, the nosniff
1511
- // header and end() rather than send(), so no ETag comes with it
1512
- response.setHeader("Content-Type", "text/plain");
1513
- response.setHeader("X-Content-Type-Options", "nosniff");
1514
- response.end(allowedMethods);
1892
+ try {
1893
+ this._sendOptionsReply(request, response);
1894
+ } catch (err) {
1895
+ // a head already written: the error answers instead, as express's does
1896
+ this._handleError(err, null, request, response);
1897
+ }
1515
1898
  return;
1516
1899
  }
1517
1900
  response.status(404);