fulmine.js 5.12.2 → 5.13.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/src/router.js CHANGED
@@ -354,6 +354,13 @@ class Walk {
354
354
  runRoute(continueRoute) {
355
355
  const req = this.req;
356
356
  const route = this.route;
357
+ // A compiled chain walks into a mount rather than entering it, so the rule above needs
358
+ // saying here as well: everything after this marker is inside the mount, and a mount is
359
+ // stepped over while an error is in flight. Leaving the chain is what running out of it
360
+ // already means, and ordinary routing takes over after the mount.
361
+ if (route.keepMount === true && req._error) {
362
+ return this.dispatch(this.routes.length);
363
+ }
357
364
  if (route.use) {
358
365
  if (route.mountApp) {
359
366
  // optimized chain: normal dispatch swaps req.app when it enters a mounted
@@ -484,7 +491,12 @@ class Walk {
484
491
  if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
485
492
  return this.step(undefined);
486
493
  }
487
- if (kind === CALLBACK_ROUTER) {
494
+ // A mounted router or application is stepped over while an error is in flight. Its handle
495
+ // takes three arguments, so express's Layer#handleError hands the error straight on without
496
+ // entering it: what a mount catches is what it raised itself. Entering it ran the error
497
+ // handlers written inside the mount, and left req.app pointing at a mounted application,
498
+ // whose settings then answered. A 500 carried an ETag under app.set("etag", false).
499
+ if (kind === CALLBACK_ROUTER && !req._error) {
488
500
  if (callback._isApplication) {
489
501
  rememberApp(this, route, req);
490
502
  useApp(req, callback);
@@ -683,10 +695,18 @@ function setMountedPath(req) {
683
695
  req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
684
696
  req._opPathLower = null;
685
697
  req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
686
- req.path = req._opPath === "" ? "/" : req._opPath;
698
+ req._path = req._opPath === "" ? "/" : req._opPath;
687
699
  req._lastUrl = req.url;
688
700
  }
689
701
 
702
+ // req.path as the request class declares it, taken off the prototype rather than written out a
703
+ // second time. A request the router adopts is a plain object and gets it defined on itself, see
704
+ // adoptPlainRequest. Enumerable, as express's own is.
705
+ const PATH_PROPERTY = {
706
+ .../** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Request.prototype, "path")),
707
+ enumerable: true
708
+ };
709
+
690
710
  const NO_PARAM_NAMES = [];
691
711
 
692
712
  /**
@@ -830,7 +850,10 @@ function adoptPlainRequest(req, router) {
830
850
  const path = queryIndex === -1 ? raw : raw.slice(0, queryIndex);
831
851
  req.urlQuery = queryIndex === -1 ? "" : raw.slice(queryIndex);
832
852
  req._rawQuery = req.urlQuery.slice(1);
833
- req.path = path;
853
+ req._path = path;
854
+ // an adopted request is a plain object, so it carries no prototype of ours and reads its path
855
+ // off a property of its own. The class's getter itself, so there is one of it
856
+ Object.defineProperty(req, "path", PATH_PROPERTY);
834
857
  req.originalUrl = req.originalUrl ?? arrived;
835
858
  req._originalPath = path;
836
859
  req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
@@ -1056,7 +1079,17 @@ function stepsOver(route, req) {
1056
1079
  if (route.bodyMethods === undefined) {
1057
1080
  route.bodyMethods = req.app.get("body methods") ?? null;
1058
1081
  }
1059
- return route.bodyMethods === null || !route.bodyMethods.includes(req.method);
1082
+ if (route.bodyMethods !== null && route.bodyMethods.includes(req.method)) {
1083
+ return false;
1084
+ }
1085
+ // The layer is not entered, so it leaves the one mark it would have left: the parser puts
1086
+ // `body` on the request before it works out that there is nothing to read. A library asks
1087
+ // `"body" in req` to tell "a parser has run" from "none has", and a skip that did not leave
1088
+ // it would answer a GET differently from express. See the same seeding in middlewares.js
1089
+ if (!("body" in req)) {
1090
+ req.body = undefined;
1091
+ }
1092
+ return true;
1060
1093
  }
1061
1094
 
1062
1095
  /**
@@ -1848,6 +1881,16 @@ module.exports = class Router extends EventEmitter {
1848
1881
  }
1849
1882
  }
1850
1883
 
1884
+ // The same rule as the one just above, which a route of another method reaches by
1885
+ // another road. A mount's chain is inherited by every path under it, and a route that
1886
+ // is not itself a mount answers the mount point rather than the subtree: in the chain
1887
+ // it ran for the whole of it, so router.all("/:p1") answered the /posts/a-b that
1888
+ // belongs to the router mounted at /posts. guardsInside is written for this: only
1889
+ // layers with more segments than the mount path are asked about a leaf.
1890
+ if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
1891
+ return false;
1892
+ }
1893
+
1851
1894
  // a RegExp mount runs only where its match starts the path and breaks on a separator,
1852
1895
  // which is decidable here against a literal path and not against one with a parameter
1853
1896
  if (r.regexMount) {
@@ -1874,13 +1917,9 @@ module.exports = class Router extends EventEmitter {
1874
1917
 
1875
1918
  // check if the paths match. A route with parameters is excluded from the text test:
1876
1919
  // its literal ":name" text would let an earlier regex in on requests it never matches.
1877
- // Both spellings of this route's path are tried, because without strict routing it
1878
- // answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
1879
- // still an earlier pattern that answers a request this registration would take.
1920
+ const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
1880
1921
  if (
1881
- (r.pattern instanceof RegExp &&
1882
- (!withParams || r.use) &&
1883
- (r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
1922
+ (regexCanMatch && r.pattern.test(route.path)) ||
1884
1923
  (typeof r.pattern === "string" &&
1885
1924
  (r.pattern === route.path ||
1886
1925
  (!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
@@ -1892,6 +1931,15 @@ module.exports = class Router extends EventEmitter {
1892
1931
  optimizedPath.push(r);
1893
1932
  continue;
1894
1933
  }
1934
+ // Without strict routing this registration answers "/x/" as well as "/x". An earlier
1935
+ // pattern matching only the second answers part of what the registration takes and not
1936
+ // the rest, which the chain has no way to say: it runs what is in it without matching
1937
+ // again. Both spellings used to put the route in whole, so app.all("/:p0/{:o1}/{:o2}")
1938
+ // answered a GET /list/Mixed that belonged to the route written after it. An ordinary
1939
+ // pattern answers both spellings, so only an optional group or a wildcard reaches here.
1940
+ if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
1941
+ return false;
1942
+ }
1895
1943
  if (!withParams) {
1896
1944
  continue;
1897
1945
  }
@@ -1951,6 +1999,13 @@ module.exports = class Router extends EventEmitter {
1951
1999
  if (!this.uwsApp) {
1952
2000
  return;
1953
2001
  }
2002
+ // Everything below is what makes this framework fast, and every one of its decisions is a
2003
+ // claim that µWS answering by itself is the same answer the chain would have given. Turned
2004
+ // off, the claim is not made and the chain answers everything. Serving one application both
2005
+ // ways and comparing the answers is what tests those claims: `npm run fuzz -- --self`.
2006
+ if (this.get("native routes") === false) {
2007
+ return;
2008
+ }
1954
2009
 
1955
2010
  // pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
1956
2011
  // carries what was written before them and answers only part of what is under them
@@ -2135,7 +2190,7 @@ module.exports = class Router extends EventEmitter {
2135
2190
  *
2136
2191
  * @param {any} response
2137
2192
  */
2138
- _refuseFraming(response) {
2193
+ _refuseRequest(response) {
2139
2194
  response.finished = true;
2140
2195
  response._res.close();
2141
2196
  response.emit("close");
@@ -2247,8 +2302,8 @@ module.exports = class Router extends EventEmitter {
2247
2302
  }
2248
2303
  const request = this.handleRequest(res, req, preset, skipHolder);
2249
2304
  const response = request.res;
2250
- if (request._badFraming === true) {
2251
- return this._refuseFraming(response);
2305
+ if (request._mustRefuse === true) {
2306
+ return this._refuseRequest(response);
2252
2307
  }
2253
2308
  if (optimizedParams) {
2254
2309
  request.optimizedParams = new NullObject();
@@ -2347,6 +2402,10 @@ module.exports = class Router extends EventEmitter {
2347
2402
  route.callbacks.length === 1 && // must not have multiple callbacks
2348
2403
  typeof route.callbacks[0] === "function" && // must be a function
2349
2404
  route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
2405
+ // a captured value is decoded when the route runs, and one that cannot be decoded is a
2406
+ // 400 in express and on the ordinary path here. Nothing runs to raise it on a
2407
+ // declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
2408
+ route.optimizedParams === undefined &&
2350
2409
  // a declarative response is answered by µWS itself, so no javascript runs and the case
2351
2410
  // guard could not: a route that needs one has to stay an ordinary handler
2352
2411
  caseGuards === null &&
package/src/testing.js CHANGED
@@ -171,6 +171,32 @@ function expectNative(app, patterns) {
171
171
  );
172
172
  }
173
173
 
174
+ /**
175
+ * Why a route µWS already matches is still not compiled into a response.
176
+ *
177
+ * The handler is the last answer, not the first: three refusals come before it, and blaming the
178
+ * handler for one of those sends the reader to rewrite something that was already simple enough.
179
+ *
180
+ * @param {any} app
181
+ * @param {{path: string}} entry
182
+ * @returns {string}
183
+ */
184
+ function whyNotCompiled(app, entry) {
185
+ if (!app.get("declarative responses")) {
186
+ return "answered by µWS, but declarative responses are turned off";
187
+ }
188
+ if (entry.path.includes(":")) {
189
+ return "answered by µWS, but the route captures, and nothing runs to decode the value";
190
+ }
191
+ if (app.get("etag")) {
192
+ return (
193
+ "answered by µWS, but a response carrying an ETag could never answer the conditional " +
194
+ 'request it invites: app.set("etag", false) is what puts a route here'
195
+ );
196
+ }
197
+ return "answered by µWS, but the handler is not simple enough to compile";
198
+ }
199
+
174
200
  /**
175
201
  * Throws unless every route named is answered from a response written at startup, which is the
176
202
  * step past native: µWS answers it without entering javascript at all.
@@ -189,9 +215,7 @@ function expectDeclarative(app, patterns) {
189
215
  .map(
190
216
  (entry) =>
191
217
  ` ${entry.method} ${entry.path}\n ` +
192
- (entry.native
193
- ? "answered by µWS, but the handler is no longer simple enough to compile"
194
- : entry.reason)
218
+ (entry.native ? whyNotCompiled(app, entry) : entry.reason)
195
219
  )
196
220
  .join("\n") +
197
221
  `\n\nRun \`npx fulmine profile\` to see the whole picture.`
package/src/utils.js CHANGED
@@ -409,8 +409,21 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
409
409
  // splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
410
410
  // cannot contain a dot. After static text there is nothing to give ground, so the
411
411
  // parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
412
- const separator = lastTokenWasParam && groupContent[0] && groupContent[0] !== ":" ? groupContent[0] : "";
413
- const groupParamClass = `[^/${separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}]+`;
412
+ // The whole of it, not its first character: /:foo{abc:bar} against /123abcabc splits as
413
+ // foo=123 and bar=abc on express, and reading the separator as "a" left bar unable to
414
+ // match its own text, so the group never matched and foo took the segment whole. More
415
+ // than one character cannot go in a class, so it is written as a lookahead, and either
416
+ // way the parameter is still allowed to be exactly the separator, as path-to-regexp
417
+ // writes it.
418
+ const colon = groupContent.indexOf(":");
419
+ const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
420
+ const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
421
+ const groupParamClass =
422
+ separator === ""
423
+ ? "[^/]+"
424
+ : separator.length === 1
425
+ ? `[^/${escapedSeparator}]+|${escapedSeparator}`
426
+ : `(?:(?!${escapedSeparator})[^/])+|${escapedSeparator}`;
414
427
 
415
428
  let groupRegex = "";
416
429
  let gi = 0;
@@ -961,6 +974,13 @@ const defaultSettings = {
961
974
  // The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
962
975
  // it routes whose earlier siblings it can prove agree under either case rule.
963
976
  "declarative responses": true,
977
+ // on. Off hands every request to the ordinary chain instead of letting µWS match what it can,
978
+ // which is slower and answers the same. Not a tuning knob: it exists so one application can be
979
+ // served both ways and the two sets of answers compared, which tests the optimizer against the
980
+ // rest of the framework without a second framework to compare with. See
981
+ // `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
982
+ // takes "declarative responses" with it.
983
+ "native routes": true,
964
984
  // off: with a window set, the size and mtime of a file served by sendFile are remembered for
965
985
  // it, which is one syscall less per request and a file that can be served as it was a moment
966
986
  // ago. "stat cache ms" is the window in milliseconds, compiled from it by set()