fulmine.js 5.15.0 → 5.15.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/README.md CHANGED
@@ -296,6 +296,10 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
296
296
 
297
297
  ## Differences from Express
298
298
 
299
+ What the two servers answer on the wire, probed from outside, malformed input and smuggling
300
+ attempts included: [fulmine.js on http-probe.com](https://www.http-probe.com/servers/fulmine-js.html)
301
+ against [express](https://www.http-probe.com/servers/express.html).
302
+
299
303
  - `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`. Runnable: [`examples/graceful-shutdown.js`](./examples/graceful-shutdown.js).
300
304
  - `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
301
305
  - request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.15.0",
3
+ "version": "5.15.1",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -29,6 +29,20 @@ const qs = require("qs");
29
29
  const parseQuery = require("./parse-query.js");
30
30
  const { kGetSafe } = require("./usage.js");
31
31
  const { AsyncResource } = require("async_hooks");
32
+
33
+ /**
34
+ * What AsyncResource.bind answers, without node's generic wrapper: that one builds a rest-args
35
+ * closure and defines properties onto it per call, ~1.9us on this node, where the resource plus
36
+ * an arrow through runInAsyncScope restores the same context for ~0.08. The type keeps the bound
37
+ * function's name, as node's does.
38
+ *
39
+ * @param {(...args: any[]) => any} fn called with at most one argument by every caller here
40
+ * @returns {(err?: any) => any}
41
+ */
42
+ function bindContext(fn) {
43
+ const resource = new AsyncResource(fn.name || "bound-anonymous-fn");
44
+ return (err) => resource.runInAsyncScope(fn, undefined, err);
45
+ }
32
46
  const {
33
47
  fastQueryParse,
34
48
  NullObject,
@@ -758,7 +772,7 @@ function serveStatic(root, options) {
758
772
  return res.sendFile(
759
773
  _path,
760
774
  options,
761
- AsyncResource.bind((e) => {
775
+ bindContext((e) => {
762
776
  if (e) {
763
777
  next(options.fallthrough && FALLTHROUGH_STATUSES.has(e.status) ? undefined : e);
764
778
  }
@@ -911,12 +925,14 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
911
925
  }
912
926
 
913
927
  const length = req._rawHeader("content-length");
928
+ // converted once: four sites read this number on the fast path
929
+ const lengthNumber = length === undefined ? NaN : +length;
914
930
 
915
931
  // No content-length and no transfer-encoding means the request carries no body at all,
916
932
  // and a body parser must leave it alone rather than parse nothing into an empty value.
917
933
  // type-is applies this before matching the type, but the simpleType shortcut below
918
934
  // compares strings directly and would otherwise skip the check.
919
- if (req._rawHeader("transfer-encoding") === undefined && isNaN(length)) {
935
+ if (req._rawHeader("transfer-encoding") === undefined && Number.isNaN(lengthNumber)) {
920
936
  return next();
921
937
  }
922
938
 
@@ -960,7 +976,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
960
976
  // {} for json and urlencoded, '' for text, an empty Buffer for raw - rather than leaving
961
977
  // req.body as the placeholder object. there is nothing to read, so run the tail directly,
962
978
  // and the verify hook still runs first: webhook signature checks rely on that
963
- if (Number(length) === 0) {
979
+ if (lengthNumber === 0) {
964
980
  req.bodyRead = true;
965
981
  const empty = Buffer.alloc(0);
966
982
  if (!runVerify(req, res, next, options, empty)) {
@@ -969,12 +985,12 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
969
985
  return beforeReturn(req, res, next, options, empty, encoding);
970
986
  }
971
987
 
972
- // skip reading too large body
973
- if (length && +length > limit) {
988
+ // skip reading too large body; NaN compares false, so no declared length passes
989
+ if (lengthNumber > limit) {
974
990
  return next(
975
991
  bodyError("request entity too large", 413, "entity.too.large", {
976
- expected: +length,
977
- length: +length,
992
+ expected: lengthNumber,
993
+ length: lengthNumber,
978
994
  limit: limit
979
995
  })
980
996
  );
@@ -1024,7 +1040,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1024
1040
  // From here the body really gets read, and uWS delivers it on native callbacks that
1025
1041
  // carry no async context, so this is the one continuation that has to be bound: an
1026
1042
  // upstream middleware's AsyncLocalStorage must still be there when next runs
1027
- next = AsyncResource.bind(next);
1043
+ next = bindContext(next);
1028
1044
 
1029
1045
  // with nothing to decompress, uWS can collect the whole body in native code: one
1030
1046
  // callback instead of one per chunk, the limit enforced before any byte reaches JS,
@@ -1032,8 +1048,8 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1032
1048
  // returns, so a view over uWS's own memory is enough. A declared length was the
1033
1049
  // original case; a chunked body accumulates in the same native vector and only loses
1034
1050
  // the length check, since there is no declaration to hold it to
1035
- const declared = Number(length);
1036
- const declaresLength = !isNaN(declared) && declared > 0;
1051
+ const declared = lengthNumber;
1052
+ const declaresLength = !Number.isNaN(declared) && declared > 0;
1037
1053
  if (!req.receivedData && !inflate && req._res.collectBody && (declaresLength || isNaN(declared))) {
1038
1054
  req.bodyRead = true;
1039
1055
  req._res.collectBody(limit, (body) => {
@@ -1358,50 +1374,58 @@ const urlencoded = createBodyParser(
1358
1374
  function (req, res, next, options, buf, encoding) {
1359
1375
  try {
1360
1376
  const body = decodeBody(buf, encoding);
1361
- const count = parameterCount(body, options.parameterLimit);
1362
- if (count === undefined) {
1363
- return next(bodyError("too many parameters", 413, "parameters.too.many"));
1364
- }
1365
1377
  // Express 5 defaults extended to false, so nested keys need opting in
1366
1378
  const extended = typeof options.extended !== "undefined" ? options.extended : false;
1367
1379
  // qs has to know the charset itself for anything but utf-8, and the sentinel options
1368
1380
  // change what a parse means, so those bodies skip the fast parsers
1369
1381
  const needsQs = encoding !== "utf-8" || options.charsetSentinel || options.interpretNumericEntities;
1370
- if (extended) {
1371
- // the ceiling body-parser gives qs: the array limit rises to the parameter count,
1372
- // so a form posting 150 array members still yields an array. count counts "&"
1373
- // separators where body-parser counts parameters, hence the + 1
1374
- const qsOptions = {
1375
- ...EXTENDED_QS_OPTIONS,
1376
- depth: options.depth !== undefined ? options.depth : 32,
1377
- arrayLimit: Math.max(100, count + 1),
1378
- charsetSentinel: options.charsetSentinel,
1379
- interpretNumericEntities: options.interpretNumericEntities,
1380
- charset: encoding,
1381
- parameterLimit: options.parameterLimit
1382
- };
1383
- req.body = needsQs
1384
- ? Object.assign(Object.create(null), qs.parse(body, qsOptions))
1385
- : fastQueryParse(body, qsOptions);
1386
- } else if (needsQs) {
1387
- // body-parser's extended: false is still qs, with depth 0 and the count as the
1388
- // array ceiling; only qs decodes latin1 percent escapes as latin1
1389
- req.body = Object.assign(
1390
- Object.create(null),
1391
- qs.parse(body, {
1392
- allowPrototypes: true,
1393
- arrayLimit: count + 1,
1394
- depth: 0,
1395
- strictDepth: true,
1382
+ if (!extended && !needsQs) {
1383
+ // the vendored parser, so an urlencoded body inspects like req.query does. The
1384
+ // parameter limit is enforced inside its scan, so the body is not walked twice;
1385
+ // assigned only when it held, so an overflow leaves req.body the placeholder
1386
+ const parsed = parseQuery(body, undefined, options.parameterLimit);
1387
+ if (parseQuery.overflow === true) {
1388
+ return next(bodyError("too many parameters", 413, "parameters.too.many"));
1389
+ }
1390
+ req.body = parsed;
1391
+ } else {
1392
+ const count = parameterCount(body, options.parameterLimit);
1393
+ if (count === undefined) {
1394
+ return next(bodyError("too many parameters", 413, "parameters.too.many"));
1395
+ }
1396
+ if (extended) {
1397
+ // the ceiling body-parser gives qs: the array limit rises to the parameter
1398
+ // count, so a form posting 150 array members still yields an array. count
1399
+ // counts "&" separators where body-parser counts parameters, hence the + 1
1400
+ const qsOptions = {
1401
+ ...EXTENDED_QS_OPTIONS,
1402
+ depth: options.depth !== undefined ? options.depth : 32,
1403
+ arrayLimit: Math.max(100, count + 1),
1396
1404
  charsetSentinel: options.charsetSentinel,
1397
1405
  interpretNumericEntities: options.interpretNumericEntities,
1398
1406
  charset: encoding,
1399
1407
  parameterLimit: options.parameterLimit
1400
- })
1401
- );
1402
- } else {
1403
- // the vendored parser, so an urlencoded body inspects like req.query does
1404
- req.body = parseQuery(body);
1408
+ };
1409
+ req.body = needsQs
1410
+ ? Object.assign(Object.create(null), qs.parse(body, qsOptions))
1411
+ : fastQueryParse(body, qsOptions);
1412
+ } else {
1413
+ // body-parser's extended: false is still qs, with depth 0 and the count as
1414
+ // the array ceiling; only qs decodes latin1 percent escapes as latin1
1415
+ req.body = Object.assign(
1416
+ Object.create(null),
1417
+ qs.parse(body, {
1418
+ allowPrototypes: true,
1419
+ arrayLimit: count + 1,
1420
+ depth: 0,
1421
+ strictDepth: true,
1422
+ charsetSentinel: options.charsetSentinel,
1423
+ interpretNumericEntities: options.interpretNumericEntities,
1424
+ charset: encoding,
1425
+ parameterLimit: options.parameterLimit
1426
+ })
1427
+ );
1428
+ }
1405
1429
  }
1406
1430
  } catch (e) {
1407
1431
  // qs reports a depth overflow as a RangeError with its own wording; body-parser
@@ -34,17 +34,30 @@ const plusRegex = /\+/g;
34
34
  * node's querystring.parse semantics on a null-prototype result: repeated keys accumulate into
35
35
  * arrays, '+' is a space, percent sequences decode when present and stay literal when broken.
36
36
  *
37
+ * `capture` collects the decoded pairs flat, key then value, so a caller can replay the stores
38
+ * without scanning again; a repeated key marks it invalid instead. See `get query`.
39
+ *
40
+ * `separatorLimit` refuses a body with that many "&" separators the way body-parser's
41
+ * parameterCount does, but inside this scan instead of a scan of its own: the overflow flag on
42
+ * the function is set, the partial result is to be discarded, and the caller answers 413.
43
+ *
37
44
  * @param {string} input
45
+ * @param {string[] & {invalid?: boolean}} [capture]
46
+ * @param {number} [separatorLimit]
38
47
  * @returns {Record<string, string | string[]>}
39
48
  */
40
- function parseQuery(input) {
49
+ function parseQuery(input, capture, separatorLimit) {
41
50
  const result = Object.create(null);
51
+ if (separatorLimit !== undefined) {
52
+ parseQuery.overflow = false;
53
+ }
42
54
 
43
55
  if (typeof input !== "string") {
44
56
  return result;
45
57
  }
46
58
 
47
59
  const inputLength = input.length;
60
+ let separators = 0;
48
61
  let key;
49
62
  let value = "";
50
63
  let startingIndex = -1;
@@ -62,6 +75,12 @@ function parseQuery(input) {
62
75
 
63
76
  // '&' or the end of the input closes the current pair
64
77
  if (c === 38) {
78
+ // real separators only, not the synthetic closing one, counted exactly as
79
+ // parameterCount counts them
80
+ if (i !== inputLength && separatorLimit !== undefined && ++separators === separatorLimit) {
81
+ parseQuery.overflow = true;
82
+ return result;
83
+ }
65
84
  hasBothKeyValuePair = equalityIndex > startingIndex;
66
85
 
67
86
  // the equality index doubles as the end of the key when there was no '='
@@ -92,7 +111,13 @@ function parseQuery(input) {
92
111
  const currentValue = result[key];
93
112
  if (currentValue === undefined) {
94
113
  result[key] = value;
114
+ if (capture !== undefined) {
115
+ capture.push(key, value);
116
+ }
95
117
  } else {
118
+ if (capture !== undefined) {
119
+ capture.invalid = true;
120
+ }
96
121
  // value.pop is cheaper than Array.isArray here, as upstream measured
97
122
  if (currentValue.pop) {
98
123
  currentValue.push(value);
@@ -134,4 +159,7 @@ function parseQuery(input) {
134
159
  return result;
135
160
  }
136
161
 
162
+ // whether the last limited call hit its separator limit, see the parameter's doc
163
+ parseQuery.overflow = false;
164
+
137
165
  module.exports = parseQuery;
package/src/request.js CHANGED
@@ -229,6 +229,13 @@ function endsWithChunked(value) {
229
229
  * @returns {boolean}
230
230
  */
231
231
  function saysClose(value) {
232
+ // what clients actually send, almost always: two interned compares answer before the scan
233
+ if (value === "keep-alive") {
234
+ return false;
235
+ }
236
+ if (value === "close") {
237
+ return true;
238
+ }
232
239
  const length = value.length;
233
240
  let at = 0;
234
241
  while (at < length) {
@@ -662,6 +669,19 @@ module.exports = class Request extends LazyReadable {
662
669
  */
663
670
  noEtag;
664
671
 
672
+ /**
673
+ * The decoded pairs of the first default-parser parse of req.query, flat key,value; false
674
+ * when it saw a repeated key, which a flat replay cannot reproduce. See `get query`.
675
+ * @type {string[]|false|undefined}
676
+ */
677
+ _querySnap;
678
+
679
+ /**
680
+ * The raw string _querySnap came from: a url rewrite replaces _rawQuery.
681
+ * @type {string|undefined}
682
+ */
683
+ _querySnapRaw;
684
+
665
685
  /**
666
686
  * Built for every request, which is why so little happens here. The headers are copied out
667
687
  * because uWS only lends them for this call, everything derived from them waits until something
@@ -681,23 +701,24 @@ module.exports = class Request extends LazyReadable {
681
701
  super();
682
702
  this._res = res;
683
703
  this._req = req;
684
- // the plain field behind the `readable` accessor, written rather than set so a request that
685
- // never streams never builds a stream
686
- this._readableFlag = true;
687
704
  if (skipHolder !== undefined && skipHolder.skipHeaders) {
688
705
  // The chain behind this registration provably never reads a header, so instead of
689
- // copying them all out of uWS the constructor asks for the four that steer the
690
- // framework itself: body framing, keep-alive, and accept for the error page a
691
- // throw could still need. A GET that does declare a body is the rare case, and
692
- // the parsers and the stream want the whole picture, so it takes the full copy.
706
+ // copying them all out of uWS the constructor asks for the ones that steer the
707
+ // framework itself: body framing, keep-alive, and the conditional pair. A GET that
708
+ // does declare a body is the rare case, and the parsers and the stream want the
709
+ // whole picture, so it takes the full copy.
693
710
  //
694
- // Seven named reads against one forEach looks like it should lose, and does not: the
695
- // seven are flat at 0.75us however many headers are on the wire, since each one is a
696
- // napi crossing and the scan behind it is nothing, while the copy pays a hop back into
697
- // JS per header and grows, 1.16us at four headers, 1.61 at eight, 2.90 at sixteen. They
698
- // do not cross, and the gap widens exactly where real traffic lives, since a browser
699
- // sends a dozen or more. The body case pays two of the seven and then copies anyway,
700
- // which is 0.2us on a request that is about to read a body.
711
+ // A handful of named reads against one forEach looks like it should lose, and does
712
+ // not: measured at seven reads they were flat at 0.75us however many headers are on
713
+ // the wire, since each one is a napi crossing and the scan behind it is nothing,
714
+ // while the copy pays a hop back into JS per header and grows, 1.16us at four
715
+ // headers, 1.61 at eight, 2.90 at sixteen. They do not cross, and the gap widens
716
+ // exactly where real traffic lives, since a browser sends a dozen or more. The body
717
+ // case pays two reads and then copies anyway, which is 0.2us on a request that is
718
+ // about to read a body.
719
+ //
720
+ // accept is not read: nothing on a granted chain consumes it, the error and 404
721
+ // pages are fixed HTML that never negotiate.
701
722
  const length = req.getHeader("content-length");
702
723
  const transferEncoding = req.getHeader("transfer-encoding");
703
724
  // A content-length of "0" declares no body and used to stay on the cheap side, but
@@ -724,12 +745,8 @@ module.exports = class Request extends LazyReadable {
724
745
  this._connectionClose = true;
725
746
  }
726
747
  }
727
- const accept = req.getHeader("accept");
728
- if (accept !== "") {
729
- entries.push("accept", accept);
730
- }
731
748
  // send consults freshness whatever the etag setting: if-none-match can be "*"
732
- // and a handler may set a validator by hand, so the conditional trio has to be
749
+ // and a handler may set a validator by hand, so the conditional pair has to be
733
750
  // really absent rather than merely uncopied
734
751
  const ifNoneMatch = req.getHeader("if-none-match");
735
752
  if (ifNoneMatch !== "") {
@@ -739,9 +756,13 @@ module.exports = class Request extends LazyReadable {
739
756
  if (ifModifiedSince !== "") {
740
757
  entries.push("if-modified-since", ifModifiedSince);
741
758
  }
742
- const cacheControl = req.getHeader("cache-control");
743
- if (cacheControl !== "") {
744
- entries.push("cache-control", cacheControl);
759
+ // fresh() answers false before it reads cache-control unless a conditional
760
+ // arrived, so the read only pays on the requests that can use it
761
+ if (ifNoneMatch !== "" || ifModifiedSince !== "") {
762
+ const cacheControl = req.getHeader("cache-control");
763
+ if (cacheControl !== "") {
764
+ entries.push("cache-control", cacheControl);
765
+ }
745
766
  }
746
767
  }
747
768
  } else {
@@ -802,18 +823,26 @@ module.exports = class Request extends LazyReadable {
802
823
  this._opPath = this._path;
803
824
  this._originalPath = this._path;
804
825
  const rawMethod = req.getCaseSensitiveMethod();
805
- this.method = rawMethod.toUpperCase();
806
- // node's parser knows a fixed set and refuses everything else; µWS takes the token as
807
- // it finds it, so a request line is anything with a space in it. Compared before the
808
- // uppercasing on purpose: a method is case sensitive, node refuses "post", and µWS
809
- // folds it to POST and serves it. Only asked of a method the framework cannot route
810
- // anyway, since a route can only be registered for one of these, see the loop that
811
- // builds the verb methods at the end of router.js
812
- if (!KNOWN_METHODS.has(rawMethod)) {
813
- this._mustRefuse = true;
826
+ if (skipHolder !== undefined && rawMethod === skipHolder.method) {
827
+ // the registration's constant, byte for byte; any other spelling, "get" that µWS
828
+ // folded here included, takes the full check below
829
+ this.method = rawMethod;
830
+ this._isOptions = skipHolder.isOptions;
831
+ this._isHead = skipHolder.isHead;
832
+ } else {
833
+ this.method = rawMethod.toUpperCase();
834
+ // node's parser knows a fixed set and refuses everything else; µWS takes the token
835
+ // as it finds it, so a request line is anything with a space in it. Compared before
836
+ // the uppercasing on purpose: a method is case sensitive, node refuses "post", and
837
+ // µWS folds it to POST and serves it. Only asked of a method the framework cannot
838
+ // route anyway, since a route can only be registered for one of these, see the loop
839
+ // that builds the verb methods at the end of router.js
840
+ if (!KNOWN_METHODS.has(rawMethod)) {
841
+ this._mustRefuse = true;
842
+ }
843
+ this._isOptions = this.method === "OPTIONS";
844
+ this._isHead = this.method === "HEAD";
814
845
  }
815
- this._isOptions = this.method === "OPTIONS";
816
- this._isHead = this.method === "HEAD";
817
846
  }
818
847
  // what the router last saw as the method. A middleware assigning another one is a rewrite,
819
848
  // which express honours because it reads req.method at every layer, and dispatch compares
@@ -839,9 +868,6 @@ module.exports = class Request extends LazyReadable {
839
868
  // null for the same reason as the two above: a request that never enters a mount never
840
869
  // needs either array, and the push sites materialize them
841
870
  this._stack = null;
842
- // how many characters of _originalPath the mounts entered so far have taken, which is
843
- // where baseUrl ends and the path below them begins
844
- this._consumed = 0;
845
871
  // whether one of them took a trailing slash, which only a RegExp mount can: see baseUrl
846
872
  this._mountSlash = false;
847
873
  this._paramStack = null;
@@ -1039,7 +1065,11 @@ module.exports = class Request extends LazyReadable {
1039
1065
  // slash, a registered path having had it removed, so almost every request answers above.
1040
1066
  let out = "";
1041
1067
  let at = 0;
1042
- for (const taken of this._stack) {
1068
+ for (let taken of this._stack) {
1069
+ // negative marks a mount that consumed the whole path, see the push in runRoute
1070
+ if (taken < 0) {
1071
+ taken = -taken;
1072
+ }
1043
1073
  const piece = this._originalPath.slice(at, at + taken);
1044
1074
  at += taken;
1045
1075
  out += piece.charCodeAt(taken - 1) === 0x2f ? piece.slice(0, -1) : piece;
@@ -1199,18 +1229,30 @@ module.exports = class Request extends LazyReadable {
1199
1229
  * path, and req.query reflects the new query string. The assigned url is relative to the
1200
1230
  * mount the request is currently in, as it is in express, so the absolute path is rebuilt
1201
1231
  * from the piece the mounts had consumed.
1232
+ *
1233
+ * @param {boolean} [leavingMount] the caller is popping the mount the rewrite happened in
1202
1234
  */
1203
- _absorbUrlRewrite() {
1204
- const newUrl = String(this.url);
1205
- const queryIndex = newUrl.indexOf("?");
1206
- const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
1235
+ _absorbUrlRewrite(leavingMount) {
1236
+ const assignedUrl = String(this.url);
1237
+ let newUrl = assignedUrl;
1207
1238
  // the prefix the mounts consumed: everything of the absolute path the relative one was not
1208
1239
  const lastQueryIndex = this._lastUrl.indexOf("?");
1209
1240
  const oldPath = lastQueryIndex === -1 ? this._lastUrl : this._lastUrl.slice(0, lastQueryIndex);
1210
- const prefix =
1211
- oldPath === "/" && !this._originalPath.endsWith("/")
1212
- ? this._originalPath
1213
- : this._originalPath.slice(0, this._originalPath.length - oldPath.length);
1241
+ let prefix;
1242
+ if (oldPath === "/" && !this._originalPath.endsWith("/")) {
1243
+ prefix = this._originalPath;
1244
+ // express's slashAdded restore, applied where express applies it, on the way out of a
1245
+ // mount that consumed the whole path: the "/" the middleware saw was invented, and the
1246
+ // rejoin strips the first character of whatever was assigned ("/found" + "target" is
1247
+ // "/foundtarget"). Inside the mount the assigned url routes as it is, as express does
1248
+ if (leavingMount === true) {
1249
+ newUrl = newUrl.slice(1);
1250
+ }
1251
+ } else {
1252
+ prefix = this._originalPath.slice(0, this._originalPath.length - oldPath.length);
1253
+ }
1254
+ const queryIndex = newUrl.indexOf("?");
1255
+ const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
1214
1256
  this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
1215
1257
  // a rewrite to "/a?" keeps its "?", as one arriving that way does
1216
1258
  this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
@@ -1220,7 +1262,9 @@ module.exports = class Request extends LazyReadable {
1220
1262
  this._opPath = newPath;
1221
1263
  this._opPathLower = null;
1222
1264
  this._mayFailDecode = null;
1223
- this._lastUrl = newUrl;
1265
+ // the assigned string, not the mangled one: the compare against req.url must go quiet or
1266
+ // the next hop absorbs the same rewrite again with a different prefix
1267
+ this._lastUrl = assignedUrl;
1224
1268
  }
1225
1269
 
1226
1270
  /**
@@ -1251,15 +1295,16 @@ module.exports = class Request extends LazyReadable {
1251
1295
  * the parse cached and handed out as itself, the sanitised value leaked into req.query here and
1252
1296
  * a handler written against express read a trimmed value where express gives it the raw one.
1253
1297
  *
1254
- * And that is why there is no cache: the fresh object comes from parsing the raw string again,
1255
- * not from copying a kept parse. As first shipped this was parse-once-copy-per-read, and the
1256
- * copy was the expensive half: Object.assign between null-prototype objects, which live in
1257
- * V8's dictionary mode, measured 638ns for a two-parameter query where parsing the same string
1258
- * measures 119ns, and on a benchmark whose every request carries such a query it cost +1.5us
1259
- * of CPU per request, which a public arena saw as -8% on its query-carrying rows. A handler
1260
- * that reads req.query once per request now pays exactly what it paid when the parse was
1261
- * cached, one parse, and a handler that reads it N times pays N parses, which is express's
1262
- * own cost shape.
1298
+ * And that is why there is no cache of the object: the fresh object comes from the raw
1299
+ * string, not from copying a kept parse. As first shipped this was parse-once-copy-per-read,
1300
+ * and the copy was the expensive half: Object.assign between null-prototype objects, which
1301
+ * live in V8's dictionary mode, measured 638ns for a two-parameter query where parsing the
1302
+ * same string measures 119ns, and on a benchmark whose every request carries such a query it
1303
+ * cost +1.5us of CPU per request, which a public arena saw as -8% on its query-carrying rows.
1304
+ *
1305
+ * The default parser does keep the decoded pairs of its first parse, and a later read of the
1306
+ * same raw string replays the stores into a fresh null-prototype object: identical output,
1307
+ * still nothing shared between reads. A repeated key cannot be replayed and re-parses.
1263
1308
  *
1264
1309
  * @returns {Record<string, any>}
1265
1310
  */
@@ -1277,7 +1322,25 @@ module.exports = class Request extends LazyReadable {
1277
1322
  return Object.create(null);
1278
1323
  }
1279
1324
  if (qp === parseQuery) {
1280
- return parseQuery(this._rawQuery);
1325
+ const raw = this._rawQuery;
1326
+ if (this._querySnapRaw === raw) {
1327
+ const snap = this._querySnap;
1328
+ if (snap === false) {
1329
+ return parseQuery(raw);
1330
+ }
1331
+ const out = Object.create(null);
1332
+ const pairs = /** @type {string[]} */ (snap);
1333
+ for (let i = 0, len = pairs.length; i < len; i += 2) {
1334
+ out[pairs[i]] = pairs[i + 1];
1335
+ }
1336
+ return out;
1337
+ }
1338
+ /** @type {string[] & {invalid?: boolean}} */
1339
+ const capture = [];
1340
+ const out = parseQuery(raw, capture);
1341
+ this._querySnapRaw = raw;
1342
+ this._querySnap = capture.invalid === true ? false : capture;
1343
+ return out;
1281
1344
  }
1282
1345
  if (qp === fastQueryParse) {
1283
1346
  return Object.assign(Object.create(null), fastQueryParse(this._rawQuery));
@@ -1686,7 +1749,9 @@ module.exports = class Request extends LazyReadable {
1686
1749
  const value = entries[index + 1];
1687
1750
  // lowercase by the entries' contract, see the field declaration
1688
1751
  const key = entries[index];
1689
- if (Object.hasOwn(headers, key)) {
1752
+ // own values are never undefined, so the read answers "absent" without the hasOwn
1753
+ // call; a prototype-named header reads truthy and still takes the hasOwn check
1754
+ if (headers[key] !== undefined && Object.hasOwn(headers, key)) {
1690
1755
  if (discardedDuplicates.has(key)) {
1691
1756
  continue;
1692
1757
  }
package/src/response.js CHANGED
@@ -72,15 +72,37 @@ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
72
72
  // if a future node renames it, fall back to a private symbol rather than writing a property
73
73
  // literally named "undefined", which is what indexing with undefined would do
74
74
  const kOutHeaders = symbols.find((s) => s.toString() === "Symbol(kOutHeaders)") ?? Symbol("kOutHeaders");
75
+ // node's emitters tombstone a removed listener's slot instead of deleting it when this flag is
76
+ // set, which is what keeps _events in a stable shape. EventEmitter.init sets it, and never runs
77
+ // for the lazily-materialized response, so it is set by hand; a future rename degrades to the
78
+ // delete, not to an error
79
+ const kShapeMode =
80
+ Object.getOwnPropertySymbols(new EventEmitter()).find((s) => s.toString() === "Symbol(shapeMode)") ??
81
+ Symbol("shapeMode");
82
+ // names setHeader has validated and lowercased, so the constant names middleware writes per
83
+ // request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
84
+ // the never-throwing readers keep their plain toLowerCase
85
+ const VALIDATED_HEADER_NAMES = new Map();
75
86
  const HIGH_WATERMARK = 128 * 1024;
76
- // Statuses whose message carries no body, so no Content-Length may describe one either. 1xx is
77
- // the third case and is checked by range rather than listed.
78
- const STATUSES_WITHOUT_BODY = new Set([204, 304]);
87
+ // the exact string json() writes, so send() can skip recomputing the charset on it
88
+ const JSON_UTF8 = "application/json; charset=utf-8";
79
89
  // send's ceiling for maxAge, one year in milliseconds. Anything larger is clamped to it rather
80
90
  // than written out, since a year is already longer than any cache will honour.
81
91
  const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
82
92
 
83
93
  class Socket extends EventEmitter {
94
+ /**
95
+ * The Socket's own error listener, shared across sockets: an error on the stand-in closes it,
96
+ * which is the close the connection trackers wait for. EventEmitter calls it with this = the
97
+ * emitter.
98
+ *
99
+ * @this {any}
100
+ * @param {any} err
101
+ */
102
+ static _onError(err) {
103
+ this.emit("close");
104
+ }
105
+
84
106
  /**
85
107
  * Enough of a node socket for the middleware that reaches for one. There is no socket object
86
108
  * in uWS to hand over, so this stands in and forwards what it can to the response.
@@ -90,10 +112,10 @@ class Socket extends EventEmitter {
90
112
  constructor(response) {
91
113
  super();
92
114
  this.response = response;
115
+ this[kShapeMode] = true;
93
116
 
94
- this.on("error", (err) => {
95
- this.emit("close");
96
- });
117
+ // shared, not an arrow: one per process instead of one per materialized socket
118
+ this.on("error", Socket._onError);
97
119
  }
98
120
 
99
121
  /** Whether anything more can be written, which stops being true once the response is done. */
@@ -276,6 +298,11 @@ module.exports = class Response extends LazyWritable {
276
298
  drain: undefined
277
299
  };
278
300
  this._eventsCount = 0;
301
+ // tombstone removed listeners instead of deleting the key, as node's own streams do: the
302
+ // delete flipped this literal to dictionary mode on every on-finished cancel
303
+ this[kShapeMode] = true;
304
+ // on-finished stores its state here; declared so that store is not a shape change
305
+ this.__onFinished = null;
279
306
  this._req = req;
280
307
  // linked here rather than by the caller: the pair is built together, and a field the
281
308
  // constructor leaves unset is a shape change on whoever assigns it first
@@ -801,7 +828,9 @@ module.exports = class Response extends LazyWritable {
801
828
  // without one writes "Content-Length: 9223372036854775808" onto a 204. Those two paths keep
802
829
  // µWS's own rule, which closes for a bare "close" and not for a list.
803
830
  const closeConnection = this.req._connectionClose === true;
804
- if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
831
+ // 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
832
+ // third case, by range
833
+ if (this.statusCode === 204 || this.statusCode === 304 || this.statusCode < 200) {
805
834
  // no body and no length describing one, whatever the caller passed. node decides
806
835
  // this the same way, from the status alone, so res.status(304).end("x") sends the
807
836
  // status and nothing else on either.
@@ -905,9 +934,10 @@ module.exports = class Response extends LazyWritable {
905
934
  if (!skipContentType) {
906
935
  this.headers["content-type"] = "text/html; charset=utf-8";
907
936
  }
908
- } else if (typeof contentType === "string") {
937
+ } else if (typeof contentType === "string" && contentType !== JSON_UTF8) {
909
938
  // replaced, not only added: the body goes out as utf-8, so a content-type saying
910
- // iso-8859-1 would be describing bytes that are not there.
939
+ // iso-8859-1 would be describing bytes that are not there. The json() literal is
940
+ // already exactly that, so the common res.json answer skips the recomputation
911
941
  this.headers["content-type"] = withUtf8Charset(contentType);
912
942
  }
913
943
  } else {
@@ -1374,7 +1404,16 @@ module.exports = class Response extends LazyWritable {
1374
1404
  if (this.headersSent) {
1375
1405
  throw new Error("Cannot set headers after they are sent to the client");
1376
1406
  }
1377
- validateHeaderName(field);
1407
+ // one Map hit for a name already validated and lowercased: middleware writes the same
1408
+ // constant names on every request. Insert-only after validation, so no bad name can enter
1409
+ let key = VALIDATED_HEADER_NAMES.get(field);
1410
+ if (key === undefined) {
1411
+ validateHeaderName(field);
1412
+ key = field.toLowerCase();
1413
+ if (VALIDATED_HEADER_NAMES.size < 512) {
1414
+ VALIDATED_HEADER_NAMES.set(field, key);
1415
+ }
1416
+ }
1378
1417
  if (value === undefined) {
1379
1418
  /** @type {NodeJS.ErrnoException} */
1380
1419
  const err = new TypeError(`Invalid value "undefined" for header "${field}"`);
@@ -1388,7 +1427,7 @@ module.exports = class Response extends LazyWritable {
1388
1427
  // catch it
1389
1428
  const out = Array.isArray(value) ? value.map(String) : String(value);
1390
1429
  validateHeaderValue(field, out);
1391
- this.headers[field.toLowerCase()] = out;
1430
+ this.headers[key] = out;
1392
1431
  return this;
1393
1432
  }
1394
1433
 
@@ -1737,7 +1776,11 @@ module.exports = class Response extends LazyWritable {
1737
1776
  * @param {string} field
1738
1777
  */
1739
1778
  removeHeader(field) {
1740
- delete this.headers[field.toLowerCase()];
1779
+ const key = field.toLowerCase();
1780
+ // the delete is a runtime call, and helmet removes a header most responses never carry
1781
+ if (key in this.headers) {
1782
+ delete this.headers[key];
1783
+ }
1741
1784
  }
1742
1785
 
1743
1786
  /**
@@ -1912,7 +1955,7 @@ module.exports = class Response extends LazyWritable {
1912
1955
  */
1913
1956
  json(body) {
1914
1957
  if (!this.headers["content-type"]) {
1915
- this.headers["content-type"] = "application/json; charset=utf-8";
1958
+ this.headers["content-type"] = JSON_UTF8;
1916
1959
  }
1917
1960
  const hot = this.app._hot();
1918
1961
  return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
package/src/router.js CHANGED
@@ -242,36 +242,45 @@ class Walk {
242
242
  // frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
243
243
  // calling this per route measured 0.45us of a scan of four hundred
244
244
  router._freezeRoutingFlags();
245
- // written out rather than through a predicate handed to findIndexStartingFrom, which
246
- // was one closure per hop of every request not on a compiled chain
247
- for (; routeIndex < routes.length; routeIndex++) {
248
- const r = routes[routeIndex];
249
- // A HEAD request enters a route whose path matched even when its verb cannot serve
250
- // one: express exempts HEAD from the method check ("if (!hasMethod && method !==
251
- // 'HEAD')" in router/index.js), so the layer's parameters are captured and its
252
- // param() callbacks run before the route is dropped. Only asked when the router has
253
- // callbacks to run, since entering a route to step straight back out of it is
254
- // otherwise pure cost with nothing to show for it. runRoute steps over it.
255
- if (!(
256
- r.all ||
257
- r.method === req.method ||
258
- req._isOptions ||
259
- (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
260
- )) {
261
- // taken only to fail: _preprocessRequest decodes again and turns it into the
262
- // error, so the handlers of a route this request cannot run never see it
263
- if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
264
- break;
265
- }
266
- continue;
267
- }
268
- if (router._pathMatches(r, req)) {
269
- // matched, and then stepped over: a body parser this request gets nothing out
270
- // of costs a hop and answers with next() at the end of it
271
- if (r.bodyParserOnly === true && stepsOver(r, req)) {
245
+ if (routes === router._routes) {
246
+ // the router's own table has an index over its literal routes, so the scan visits
247
+ // the handful that could match instead of every one, see _scanFrom
248
+ routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
249
+ } else {
250
+ // a compiled chain's own array, always short: the linear scan stays.
251
+ // Written out rather than through a predicate handed to findIndexStartingFrom,
252
+ // which was one closure per hop of every request not on a compiled chain
253
+ const method = req.method;
254
+ const length = routes.length;
255
+ for (; routeIndex < length; routeIndex++) {
256
+ const r = routes[routeIndex];
257
+ // A HEAD request enters a route whose path matched even when its verb cannot
258
+ // serve one: express exempts HEAD from the method check ("if (!hasMethod &&
259
+ // method !== 'HEAD')" in router/index.js), so the layer's parameters are
260
+ // captured and its param() callbacks run before the route is dropped. Only
261
+ // asked when the router has callbacks to run, since entering a route to step
262
+ // straight back out of it is otherwise pure cost. runRoute steps over it.
263
+ if (!(
264
+ r.all ||
265
+ r.method === method ||
266
+ req._isOptions ||
267
+ (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
268
+ )) {
269
+ // taken only to fail: _preprocessRequest decodes again and turns it into
270
+ // the error, so the handlers of a route this request cannot run never see it
271
+ if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
272
+ break;
273
+ }
272
274
  continue;
273
275
  }
274
- break;
276
+ if (router._pathMatches(r, req)) {
277
+ // matched, and then stepped over: a body parser this request gets nothing
278
+ // out of costs a hop and answers with next() at the end of it
279
+ if (r.bodyParserOnly === true && stepsOver(r, req)) {
280
+ continue;
281
+ }
282
+ break;
283
+ }
275
284
  }
276
285
  }
277
286
  }
@@ -402,7 +411,11 @@ class Walk {
402
411
  useApp(req, route.mountApp);
403
412
  }
404
413
  const taken = mountPrefixLength(route, req);
405
- (req._stack ??= []).push(taken);
414
+ // pushed negative when this mount consumes the whole remaining path: express invents
415
+ // the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
416
+ (req._stack ??= []).push(
417
+ taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
418
+ );
406
419
  // a use with no path consumes nothing, so everything below would work out the values
407
420
  // that are already there. Only skipped without a trailing slash, where the rules about
408
421
  // one cannot bite. An application is mostly pathless middleware, and this is per hop
@@ -466,6 +479,68 @@ class Walk {
466
479
  return this.step(undefined);
467
480
  }
468
481
 
482
+ /**
483
+ * Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
484
+ * route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
485
+ * here without re-running step's prologue and compares.
486
+ *
487
+ * @param {boolean} isRouter next("router") rather than next("route")
488
+ */
489
+ leaveHop(isRouter) {
490
+ const req = this.req;
491
+ const route = this.route;
492
+ if (route.use && !route.keepMount) {
493
+ const pushed = req._stack.pop();
494
+ const taken = pushed < 0 ? -pushed : pushed;
495
+ // a rewrite done inside this middleware is taken now: the pop below recomputes
496
+ // req.url from the original path and would silently revert it. The slashAdded
497
+ // mangle belongs to the mount that consumed a prefix, not to a pathless use
498
+ if (req.url !== req._lastUrl) {
499
+ req._absorbUrlRewrite(taken !== 0);
500
+ req._consumed -= taken;
501
+ setMountedPath(req);
502
+ } else {
503
+ if (pushed < 0 && req._originalPath.length > req._consumed) {
504
+ // a rewrite below this mount left a remainder where entry had none: express
505
+ // strips the first character of it when it rejoins, see issue #17
506
+ req._originalPath =
507
+ req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
508
+ req._mayFailDecode = null;
509
+ }
510
+ if (taken !== 0) {
511
+ // a pathless use consumed nothing and rewrote nothing, so the recompute would
512
+ // write back the very values it reads
513
+ req._consumed -= taken;
514
+ setMountedPath(req);
515
+ }
516
+ }
517
+ restoreApp(route, req);
518
+ }
519
+ if (isRouter) {
520
+ if (this.skipCheck) {
521
+ // on a compiled chain, leaving the router is what running out of chain
522
+ // already means: ordinary routing takes over after the mount. With no
523
+ // mount in the chain the router being left is the app's own, and nothing
524
+ // of it may run afterwards, not even a middleware registered later
525
+ if (this.skipUntil?.keepMount) {
526
+ return this.dispatch(this.routes.length);
527
+ }
528
+ return this.resolve(false);
529
+ }
530
+ // out of this router entirely, so whoever mounted it carries on after the
531
+ // mount. The app's own walk has nobody after it, and answers 404
532
+ return this.resolve(false);
533
+ }
534
+ req.routeCount++;
535
+ // dispatch is a plain call, so a synchronous throw would escape here instead of
536
+ // rejecting, as it used to when this recursed through the async _routeRequest
537
+ try {
538
+ return this.dispatch(this.routeIndex + 1);
539
+ } catch (err) {
540
+ return this.reject(err);
541
+ }
542
+ }
543
+
469
544
  /**
470
545
  * One hop, which is what next() does: with nothing, run the route's next callback; with "route",
471
546
  * leave the route; with anything else, remember it as the error and carry on.
@@ -479,39 +554,7 @@ class Walk {
479
554
  const router = this.router;
480
555
  if (thingamabob) {
481
556
  if (thingamabob === "route" || thingamabob === "router") {
482
- if (route.use && !route.keepMount) {
483
- // a rewrite done inside this middleware is taken now: the pop below recomputes
484
- // req.url from the original path and would silently revert it
485
- if (req.url !== req._lastUrl) {
486
- req._absorbUrlRewrite();
487
- }
488
- req._consumed -= req._stack.pop();
489
- setMountedPath(req);
490
- restoreApp(route, req);
491
- }
492
- if (thingamabob === "router") {
493
- if (this.skipCheck) {
494
- // on a compiled chain, leaving the router is what running out of chain
495
- // already means: ordinary routing takes over after the mount. With no
496
- // mount in the chain the router being left is the app's own, and nothing
497
- // of it may run afterwards, not even a middleware registered later
498
- if (this.skipUntil?.keepMount) {
499
- return this.dispatch(this.routes.length);
500
- }
501
- return this.resolve(false);
502
- }
503
- // out of this router entirely, so whoever mounted it carries on after the
504
- // mount. The app's own walk has nobody after it, and answers 404
505
- return this.resolve(false);
506
- }
507
- req.routeCount++;
508
- // dispatch is a plain call, so a synchronous throw would escape here instead of
509
- // rejecting, as it used to when this recursed through the async _routeRequest
510
- try {
511
- return this.dispatch(this.routeIndex + 1);
512
- } catch (err) {
513
- return this.reject(err);
514
- }
557
+ return this.leaveHop(thingamabob === "router");
515
558
  } else {
516
559
  req._error = thingamabob;
517
560
  req._errorKey = route.routeKey;
@@ -521,7 +564,7 @@ class Walk {
521
564
  const kind = route.callbackKinds[this.callbackIndex];
522
565
  const callback = route.callbacks[this.callbackIndex++];
523
566
  if (!callback) {
524
- return this.step("route");
567
+ return this.leaveHop(false);
525
568
  }
526
569
  // skipping routes we already went through via optimized path. Before the Router branch
527
570
  // below and not after it: a mount whose chain was compiled has already run, and running it
@@ -715,6 +758,10 @@ function mountPrefixLength(route, req) {
715
758
  if (route.pattern === EMPTY_REGEX) {
716
759
  return 0;
717
760
  }
761
+ // the registration-time constant of a literal mount, exec-free. See createRoute
762
+ if (route.mountLen !== undefined) {
763
+ return route.mountLen;
764
+ }
718
765
  if (typeof route.pattern === "string") {
719
766
  return route.pattern.length;
720
767
  }
@@ -801,6 +848,61 @@ function mergesParams(route, fallback) {
801
848
  return Boolean(owner?._settings?.mergeParams);
802
849
  }
803
850
 
851
+ // shared empty candidate list, so _scanFrom never tests for a missing map entry twice
852
+ const EMPTY_INDICES = /** @type {number[]} */ ([]);
853
+
854
+ /**
855
+ * The generic scan's index over a router's literal routes: route positions by folded pattern, so
856
+ * a scan visits the routes registered for this exact path instead of comparing every one. String
857
+ * patterns are pure literals, everything else, "/*" included, stays in alwaysVisit and is still
858
+ * matched per request by _pathMatches.
859
+ *
860
+ * @param {any[]} routes the router's own table
861
+ * @param {boolean} caseFlag the frozen case-sensitivity flag
862
+ * @returns {{map: Map<string, number[]>, alwaysVisit: number[]}}
863
+ */
864
+ function buildLiteralIndex(routes, caseFlag) {
865
+ const map = new Map();
866
+ const alwaysVisit = [];
867
+ for (let i = 0; i < routes.length; i++) {
868
+ const pattern = routes[i].pattern;
869
+ if (typeof pattern === "string" && pattern !== "/*") {
870
+ const key = caseFlag ? pattern : routes[i].patternLower;
871
+ const list = map.get(key);
872
+ if (list === undefined) {
873
+ map.set(key, [i]);
874
+ } else {
875
+ list.push(i);
876
+ }
877
+ } else {
878
+ alwaysVisit.push(i);
879
+ }
880
+ }
881
+ return { map, alwaysVisit };
882
+ }
883
+
884
+ /**
885
+ * The position of the first value >= from in an ascending list, which is list.length when there
886
+ * is none: where a scan resuming at `from` enters a candidate list.
887
+ *
888
+ * @param {number[]} list
889
+ * @param {number} from
890
+ * @returns {number}
891
+ */
892
+ function firstAtLeast(list, from) {
893
+ let low = 0;
894
+ let high = list.length;
895
+ while (low < high) {
896
+ const mid = (low + high) >> 1;
897
+ if (list[mid] < from) {
898
+ low = mid + 1;
899
+ } else {
900
+ high = mid;
901
+ }
902
+ }
903
+ return low;
904
+ }
905
+
804
906
  /**
805
907
  * The route's own params merged with those of the mounts it sits under, in express's order: an
806
908
  * outer mount first, the route's own last. Numbered captures do not overwrite each other, they
@@ -1712,6 +1814,90 @@ module.exports = class Router extends EventEmitter {
1712
1814
  return fullMountpath;
1713
1815
  }
1714
1816
 
1817
+ /**
1818
+ * The generic scan over this router's own table, driven by the literal index: only the routes
1819
+ * registered for this exact path, plus every non-literal route, are visited, in registration
1820
+ * order, and each visited one still answers through the same method gate and _pathMatches the
1821
+ * plain loop used. The routes skipped are exactly the literals whose string compare provably
1822
+ * fails, so the first index this answers is the one the plain loop found.
1823
+ *
1824
+ * Runs after _freezeRoutingFlags, which is what makes _caseFlag and _strictFlag readable here
1825
+ * and the lazily built index stable.
1826
+ *
1827
+ * @param {any} req
1828
+ * @param {number} startIndex where to resume the scan
1829
+ * @param {boolean} mayFailDecode whether the path carries a percent escape
1830
+ * @returns {number} the index of the route to enter, or the table length for none
1831
+ */
1832
+ _scanFrom(req, startIndex, mayFailDecode) {
1833
+ const routes = this._routes;
1834
+ const index = (this._literalIndex ??= buildLiteralIndex(routes, /** @type {boolean} */ (this._caseFlag)));
1835
+ let path = req._opPath;
1836
+ if (path === "") {
1837
+ path = "/";
1838
+ }
1839
+ if (!this._caseFlag) {
1840
+ path = req._opPathLower ??= path.toLowerCase();
1841
+ }
1842
+ const exact = index.map.get(path) ?? EMPTY_INDICES;
1843
+ // the trailing-slash twin _pathMatches allows outside strict routing, as a key: a path
1844
+ // "/a/" can only text-match a literal "/a", so that list joins the candidates
1845
+ const slashed =
1846
+ !this._strictFlag && path.charCodeAt(path.length - 1) === 0x2f
1847
+ ? (index.map.get(path.slice(0, -1)) ?? EMPTY_INDICES)
1848
+ : EMPTY_INDICES;
1849
+ const always = index.alwaysVisit;
1850
+ let exactAt = firstAtLeast(exact, startIndex);
1851
+ let slashedAt = firstAtLeast(slashed, startIndex);
1852
+ let alwaysAt = firstAtLeast(always, startIndex);
1853
+ const method = req.method;
1854
+ const none = routes.length;
1855
+ for (;;) {
1856
+ // the next candidate in registration order, from whichever list holds it
1857
+ let routeIndex = none;
1858
+ if (exactAt < exact.length && exact[exactAt] < routeIndex) {
1859
+ routeIndex = exact[exactAt];
1860
+ }
1861
+ if (slashedAt < slashed.length && slashed[slashedAt] < routeIndex) {
1862
+ routeIndex = slashed[slashedAt];
1863
+ }
1864
+ if (alwaysAt < always.length && always[alwaysAt] < routeIndex) {
1865
+ routeIndex = always[alwaysAt];
1866
+ }
1867
+ if (routeIndex === none) {
1868
+ return none;
1869
+ }
1870
+ if (exactAt < exact.length && exact[exactAt] === routeIndex) {
1871
+ exactAt++;
1872
+ }
1873
+ if (slashedAt < slashed.length && slashed[slashedAt] === routeIndex) {
1874
+ slashedAt++;
1875
+ }
1876
+ if (alwaysAt < always.length && always[alwaysAt] === routeIndex) {
1877
+ alwaysAt++;
1878
+ }
1879
+ const r = routes[routeIndex];
1880
+ // the same gates as the plain loop, comments and all: see dispatch
1881
+ if (!(
1882
+ r.all ||
1883
+ r.method === method ||
1884
+ req._isOptions ||
1885
+ (req._isHead && (r.gettable || r.paramCallbacks.size > 0))
1886
+ )) {
1887
+ if (mayFailDecode && this._pathMatches(r, req) && this._paramsFailToDecode(r, req)) {
1888
+ return routeIndex;
1889
+ }
1890
+ continue;
1891
+ }
1892
+ if (this._pathMatches(r, req)) {
1893
+ if (r.bodyParserOnly === true && stepsOver(r, req)) {
1894
+ continue;
1895
+ }
1896
+ return routeIndex;
1897
+ }
1898
+ }
1899
+ }
1900
+
1715
1901
  /**
1716
1902
  * Whether a route's path matches this request. A plain string compares directly, which is what
1717
1903
  * makes a route eligible for the native router; anything carrying a parameter or a wildcard was
@@ -1887,6 +2073,13 @@ module.exports = class Router extends EventEmitter {
1887
2073
  // the "body methods" setting as it stood the first time this layer was reached,
1888
2074
  // kept the way the parser behind it keeps it. undefined until then
1889
2075
  bodyMethods: undefined,
2076
+ // a literal mount consumes exactly its registered text, so what the per-hop exec
2077
+ // in mountPrefixLength answers is a constant. "/" stays with the exec: its clamp
2078
+ // against a parent that consumed everything is not a constant
2079
+ mountLen:
2080
+ method === "USE" && typeof path === "string" && path.length > 1 && !/[:*{\\]/.test(path)
2081
+ ? path.length
2082
+ : undefined,
1890
2083
  // a mount written as a RegExp matches a piece of path that is not known until a
1891
2084
  // request comes in, so its stack entry cannot be the path itself
1892
2085
  regexMount: method === "USE" && path instanceof RegExp,
@@ -1937,6 +2130,8 @@ module.exports = class Router extends EventEmitter {
1937
2130
  routes.push(route);
1938
2131
  }
1939
2132
  this._routes.push(...routes);
2133
+ // the literal index positions are stale the moment the table grows
2134
+ this._literalIndex = undefined;
1940
2135
 
1941
2136
  // anything registered after listen invalidates what the header-skip analysis proved:
1942
2137
  // it could catch a throw or read what a chain never did, so every skip is taken back
@@ -2396,14 +2591,23 @@ module.exports = class Router extends EventEmitter {
2396
2591
  needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
2397
2592
  )
2398
2593
  : null;
2399
- const makeHandler = (chain, preset, skips) => {
2594
+ const makeHandler = (chain, preset, skips, wireMethod) => {
2400
2595
  // the mutable object a granted skip lives on, so a middleware arriving after
2401
2596
  // listen can take it back: a literal registration's preset doubles as it, and a
2402
- // parameterised one, which has no preset, gets a holder of its own
2597
+ // parameterised one, which has no preset, gets a holder of its own. It also carries
2598
+ // the registration's method, so the constructor settles it with one compare
2403
2599
  let skipHolder = preset;
2404
- if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
2405
- skipHolder = { skipHeaders: skips.skipHeaders, skipQuery: skips.skipQuery };
2406
- (this._skipPresets ??= new Set()).add(skipHolder);
2600
+ if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
2601
+ skipHolder = {
2602
+ skipHeaders: skips.skipHeaders,
2603
+ skipQuery: skips.skipQuery,
2604
+ method: wireMethod,
2605
+ isOptions: wireMethod === "OPTIONS",
2606
+ isHead: wireMethod === "HEAD"
2607
+ };
2608
+ if (skips.skipHeaders || skips.skipQuery) {
2609
+ (this._skipPresets ??= new Set()).add(skipHolder);
2610
+ }
2407
2611
  }
2408
2612
  // all three are registration-time constants: computing them in the handler was a
2409
2613
  // closure and a scan of the chain on every native request.
@@ -2430,6 +2634,8 @@ module.exports = class Router extends EventEmitter {
2430
2634
  return this._refuseRequest(response);
2431
2635
  }
2432
2636
  if (optimizedParams) {
2637
+ // slicing these out of the already-fetched path instead measured a wash:
2638
+ // the segment scan costs what the crossing costs
2433
2639
  request.optimizedParams = new NullObject();
2434
2640
  for (let i = 0; i < optimizedParams.length; i++) {
2435
2641
  request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
@@ -2475,8 +2681,8 @@ module.exports = class Router extends EventEmitter {
2475
2681
  // analysis in usage.js, whose default answer is no.
2476
2682
  //
2477
2683
  // The etag setting is not one of the conditions. It used to be, on the grounds that send
2478
- // consults freshness, but the skip branch reads if-none-match, if-modified-since and
2479
- // cache-control by name whatever the setting, see the comment at request.js:527, and
2684
+ // consults freshness, but the skip branch reads if-none-match and if-modified-since by
2685
+ // name whatever the setting, see the comment at request.js:527, and
2480
2686
  // req.fresh reads nothing else off the request. Requiring etag off as well cost the copy
2481
2687
  // to every application that left it on, which is every application that did not go
2482
2688
  // looking for the setting.
@@ -2508,10 +2714,13 @@ module.exports = class Router extends EventEmitter {
2508
2714
  return preset;
2509
2715
  };
2510
2716
 
2717
+ // the wire token this registration answers; "any" serves every verb and stays dynamic
2718
+ const wireMethod = method === "any" ? null : route.method;
2511
2719
  let fn = makeHandler(
2512
2720
  getChain,
2513
2721
  canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
2514
- getSkips
2722
+ getSkips,
2723
+ wireMethod
2515
2724
  );
2516
2725
  const jsFn = fn;
2517
2726
 
@@ -2564,7 +2773,12 @@ module.exports = class Router extends EventEmitter {
2564
2773
  fn !== jsFn
2565
2774
  ? fn
2566
2775
  : canPreset
2567
- ? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips)
2776
+ ? makeHandler(
2777
+ getChain,
2778
+ makePreset(route.path + "/", route.method, getSkips),
2779
+ getSkips,
2780
+ wireMethod
2781
+ )
2568
2782
  : fn;
2569
2783
  this.uwsApp[method](replacedPath + "/", slashFn);
2570
2784
  if (method === "get") {
@@ -2573,7 +2787,8 @@ module.exports = class Router extends EventEmitter {
2573
2787
  makeHandler(
2574
2788
  headChain,
2575
2789
  canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
2576
- headSkips
2790
+ headSkips,
2791
+ "HEAD"
2577
2792
  )
2578
2793
  );
2579
2794
  }
@@ -2582,7 +2797,12 @@ module.exports = class Router extends EventEmitter {
2582
2797
  // its own handler always: the shared one would carry the GET registration's method
2583
2798
  this.uwsApp.head(
2584
2799
  replacedPath,
2585
- makeHandler(headChain, canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined, headSkips)
2800
+ makeHandler(
2801
+ headChain,
2802
+ canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined,
2803
+ headSkips,
2804
+ "HEAD"
2805
+ )
2586
2806
  );
2587
2807
  }
2588
2808
  }
@@ -2741,14 +2961,16 @@ module.exports = class Router extends EventEmitter {
2741
2961
  raiseDecodeFailure(req, route, err);
2742
2962
  return "route";
2743
2963
  }
2744
- if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
2964
+ // the stack check first: it is two field loads, mergesParams is a call, and almost
2965
+ // no request carries a param stack at all
2966
+ if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
2745
2967
  req.params = mergeParams(req.params, req._paramStack);
2746
2968
  }
2747
2969
  } else {
2748
2970
  // express 5 gives every matched route null-prototype params; only a pathless
2749
2971
  // middleware layer keeps the plain object, as its router hands one to fast_slash
2750
2972
  req.params = route.use && route.path === "" ? {} : Object.create(null);
2751
- if (mergesParams(route, this) && req._paramStack !== null && req._paramStack.length > 0) {
2973
+ if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
2752
2974
  req.params = mergeParams(req.params, req._paramStack);
2753
2975
  }
2754
2976
  }
@@ -3132,8 +3354,12 @@ module.exports = class Router extends EventEmitter {
3132
3354
  return;
3133
3355
  }
3134
3356
  response.status(404);
3135
- // the whole path, not what a mount left behind in req.path
3136
- this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
3357
+ // the pathname of originalUrl, as express's finalhandler prints it: _originalPath absorbs
3358
+ // a req.url rewrite, originalUrl never changes
3359
+ const originalUrl = String(request.originalUrl);
3360
+ const queryIndex = originalUrl.indexOf("?");
3361
+ const pathname = queryIndex === -1 ? originalUrl : originalUrl.slice(0, queryIndex);
3362
+ this._sendErrorPage(request, response, `Cannot ${request.method} ${pathname}`, false);
3137
3363
  }
3138
3364
  };
3139
3365
 
package/src/utils.js CHANGED
@@ -1510,6 +1510,11 @@ function validateHeaderName(name) {
1510
1510
  }
1511
1511
  }
1512
1512
 
1513
+ // values already accepted, so the constant strings middleware writes per request, helmet's CSP
1514
+ // among them, skip the scan. Insert-only after the regex accepts, so a hit cannot change a
1515
+ // verdict; bounded, and set-cookie values are per-user so they stay out
1516
+ const KNOWN_HEADER_VALUES = new Set();
1517
+
1513
1518
  /**
1514
1519
  * Refuses a header value holding a character that cannot go on the wire, with node's error. An
1515
1520
  * array is sent as one header per entry, so each entry is checked on its own rather than as the
@@ -1527,9 +1532,19 @@ function validateHeaderValue(name, value) {
1527
1532
  }
1528
1533
  return;
1529
1534
  }
1535
+ if (KNOWN_HEADER_VALUES.has(value)) {
1536
+ return;
1537
+ }
1530
1538
  if (HEADER_VALUE.test(value)) {
1531
1539
  throw headerError(`Invalid character in header content ["${name}"]`, "ERR_INVALID_CHAR");
1532
1540
  }
1541
+ if (
1542
+ KNOWN_HEADER_VALUES.size < 512 &&
1543
+ value.length <= 1024 &&
1544
+ !(name.length === 10 && name.toLowerCase() === "set-cookie")
1545
+ ) {
1546
+ KNOWN_HEADER_VALUES.add(value);
1547
+ }
1533
1548
  }
1534
1549
 
1535
1550
  /**