fulmine.js 5.21.3 → 5.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.21.3",
3
+ "version": "5.21.4",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js, up to 20x faster. Same API, your middleware and framework keep working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -20,6 +20,7 @@
20
20
  },
21
21
  "scripts": {
22
22
  "test": "node tests/index.js",
23
+ "gates": "node tools/gates.js",
23
24
  "test:unit": "node --expose-gc --test --test-timeout=120000 \"tests/unit/*.test.js\"",
24
25
  "test:types": "tsd --files tests/types/*.test-d.ts",
25
26
  "test:express": "node tools/express-suite.js",
@@ -137,10 +138,13 @@
137
138
  "@release-it/conventional-changelog": "^12.0.0",
138
139
  "@types/accepts": "^1.3.7",
139
140
  "@types/bytes": "^3.1.5",
141
+ "@types/compressible": "^2.0.3",
140
142
  "@types/content-disposition": "^0.5.9",
143
+ "@types/cookie": "^0.6.0",
141
144
  "@types/cookie-signature": "^1.1.2",
142
145
  "@types/encodeurl": "^1.0.3",
143
146
  "@types/etag": "^1.8.4",
147
+ "@types/fast-decode-uri-component": "^1.0.0",
144
148
  "@types/fresh": "^0.5.3",
145
149
  "@types/mime-types": "^3.0.1",
146
150
  "@types/ms": "^2.1.0",
package/src/adopt.js CHANGED
@@ -36,7 +36,10 @@ const UWS_SPEC = /** @type {string} */ (require("../package.json").dependencies[
36
36
  /** The pnpm override that drops this package's own copy, so the project's direct one is the only one. */
37
37
  const UWS_OVERRIDE = `${SELF}>${UWS}`;
38
38
 
39
- /** Where each manager keeps its substitutions, and what to call it when telling someone. */
39
+ /**
40
+ * Where each manager keeps its substitutions, and what to call it when telling someone.
41
+ * @type {Record<string, {keys: string[], reinstall: string}>}
42
+ */
40
43
  const MANAGERS = {
41
44
  npm: { keys: ["overrides"], reinstall: "npm install" },
42
45
  pnpm: { keys: ["pnpm", "overrides"], reinstall: "pnpm install" },
@@ -41,6 +41,9 @@ const { registerWebSocketRoutes } = require("./websocket.js");
41
41
  const { addServerMembers } = require("./server-shape.js");
42
42
  const { workerCount, forkWorkers, isSupervising, becomeSupervisor } = require("./cluster.js");
43
43
 
44
+ /** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
45
+ /** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
46
+
44
47
  const cpuCount = os.cpus().length;
45
48
 
46
49
  // marks a "trust proxy" the application never set, under express's key, so a sub-app may inherit
@@ -95,12 +98,14 @@ class Application extends Router {
95
98
  /**
96
99
  * A mounted app's settings chain onto its parent's, as in express.
97
100
  *
101
+ * @override
98
102
  * @type {boolean}
99
103
  */
100
104
  _inheritsSettings = true;
101
105
 
102
106
  /**
103
107
  * See Router#_isApplication.
108
+ * @override
104
109
  * @type {boolean}
105
110
  */
106
111
  _isApplication = true;
@@ -141,7 +146,7 @@ class Application extends Router {
141
146
  /** @type {Response} */
142
147
  response;
143
148
 
144
- /** @type {boolean} */
149
+ /** @override @type {boolean} */
145
150
  listenCalled = false;
146
151
 
147
152
  /** @type {FSWorker[]} */
@@ -487,6 +492,7 @@ class Application extends Router {
487
492
  * @param {import("./router-utils.js").NativePreset} [preset] see nativePreset
488
493
  * @param {import("./router-utils.js").SkipHolder} [skipHolder] forwarded whole, dropping it
489
494
  * silently turned every skip off
495
+ * @override
490
496
  * @returns {Request} the request, with the response as request.res
491
497
  */
492
498
  handleRequest(res, req, preset, skipHolder) {
@@ -509,6 +515,7 @@ class Application extends Router {
509
515
  * The µWS app, for what µWS offers that this does not (socket.io attaches to it). Made on
510
516
  * first ask, so an app served through node's http never loads the binary, see src/uws.js.
511
517
  *
518
+ * @override
512
519
  * @returns {any}
513
520
  */
514
521
  get uwsApp() {
@@ -521,7 +528,9 @@ class Application extends Router {
521
528
 
522
529
  /** The catch-all uWS handler, for every request no native route took. */
523
530
  _createRequestHandler() {
524
- this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
531
+ this.uwsApp.any("/*", (/** @type {UwsResponse} */ res, /** @type {UwsRequest} */ req) =>
532
+ this._serveGeneric(res, req)
533
+ );
525
534
  }
526
535
 
527
536
  /**
package/src/cli.js CHANGED
@@ -41,6 +41,7 @@ const TO = "fulmine.js";
41
41
 
42
42
  // modules this has a faster version of, reported and not rewritten: the replacement lives on the
43
43
  // express import, which may not be in scope where these are required
44
+ /** @type {Record<string, string>} */
44
45
  const BUILT_IN_INSTEAD = {
45
46
  compression: "express.compression(), which takes the same options",
46
47
  "serve-static": "express.static()",
@@ -20,6 +20,12 @@ limitations under the License.
20
20
  * What res.on was given, parked until there is a compressor to hang it on.
21
21
  * @typedef {Parameters<import("stream").Writable["on"]>} OnArgs
22
22
  */
23
+ /**
24
+ * What the hooks below take: writeHead's arguments in node's two shapes, see applyWriteHead, and
25
+ * the callback write() and end() may carry in place of the encoding.
26
+ * @typedef {Parameters<Response["writeHead"]>} WriteHeadArgs
27
+ * @typedef {(err?: Error|null) => void} WriteCallback
28
+ */
23
29
 
24
30
  // express.compression(): the compression module's options, defaults and decision order, with three
25
31
  // differences: a whole body (res.send, res.json) is compressed in one call and goes out with a
@@ -229,8 +235,10 @@ function toBuffer(chunk, encoding) {
229
235
  * compressed at all. The default says yes to any compressible content type.
230
236
  * @param {string} [options.enforceEncoding] what to use when the request carries no
231
237
  * Accept-Encoding at all. Default "identity", which is to say nothing is compressed.
232
- * @param {object} [options.brotli] brotli options, `params` included. The default quality is 4.
233
- * @param {object} [options.zstd] zstd options, `params` included, node's own defaults otherwise.
238
+ * @param {import("zlib").BrotliOptions} [options.brotli] brotli options, `params` included. The
239
+ * default quality is 4.
240
+ * @param {import("zlib").ZstdOptions} [options.zstd] zstd options, `params` included, node's own
241
+ * defaults otherwise.
234
242
  * @param {string[]} [options.encodings] the encodings this middleware may answer with, out of
235
243
  * "br", "zstd", "gzip" and "deflate". What is not named is never used, however the client ranks
236
244
  * it. An uncompressed answer is always on offer, and enforceEncoding is outside this list. This
@@ -251,7 +259,7 @@ function compression(options) {
251
259
  const brotliOptions = { ...opts.brotli };
252
260
  brotliOptions.params = {
253
261
  [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
254
- ...(opts.brotli && /** @type {import("zlib").BrotliOptions} */ (opts.brotli).params)
262
+ ...(opts.brotli && opts.brotli.params)
255
263
  };
256
264
  // zstd at node's default level is already in the band this middleware wants
257
265
  const zstdOptions = { ...opts.zstd };
@@ -380,11 +388,21 @@ function compression(options) {
380
388
  }
381
389
  };
382
390
  // at writeHead too, its headers applied first as on-headers orders it
391
+ /**
392
+ * @param {WriteHeadArgs[0]} statusCode
393
+ * @param {WriteHeadArgs[1]} [statusMessage]
394
+ * @param {WriteHeadArgs[2]} [headers]
395
+ */
383
396
  res.writeHead = function writeHead(statusCode, statusMessage, headers) {
384
397
  const reason = applyWriteHead(this, statusMessage, headers);
385
398
  vary();
386
399
  return _plainWriteHead.call(this, statusCode, reason);
387
400
  };
401
+ /**
402
+ * @param {any} [chunk]
403
+ * @param {BufferEncoding|WriteCallback} [encoding]
404
+ * @param {WriteCallback} [callback]
405
+ */
388
406
  res.end = function end(chunk, encoding, callback) {
389
407
  vary();
390
408
  return _plainEnd.call(this, chunk, encoding, callback);
@@ -492,6 +510,11 @@ function compression(options) {
492
510
  }
493
511
 
494
512
  // decided at writeHead too, its headers applied first as on-headers orders it
513
+ /**
514
+ * @param {WriteHeadArgs[0]} statusCode
515
+ * @param {WriteHeadArgs[1]} [statusMessage]
516
+ * @param {WriteHeadArgs[2]} [headers]
517
+ */
495
518
  res.writeHead = function writeHead(statusCode, statusMessage, headers) {
496
519
  const reason = applyWriteHead(this, statusMessage, headers);
497
520
  if (!decided) {
@@ -500,6 +523,11 @@ function compression(options) {
500
523
  return _writeHead.call(this, statusCode, reason);
501
524
  };
502
525
 
526
+ /**
527
+ * @param {any} chunk
528
+ * @param {BufferEncoding|WriteCallback} [encoding]
529
+ * @param {WriteCallback} [callback]
530
+ */
503
531
  res.write = function write(chunk, encoding, callback) {
504
532
  if (typeof encoding === "function") {
505
533
  callback = encoding;
@@ -520,6 +548,11 @@ function compression(options) {
520
548
  return _write.call(this, chunk, encoding, callback);
521
549
  };
522
550
 
551
+ /**
552
+ * @param {any} [chunk]
553
+ * @param {BufferEncoding|WriteCallback} [encoding]
554
+ * @param {WriteCallback} [callback]
555
+ */
523
556
  res.end = function end(chunk, encoding, callback) {
524
557
  if (typeof chunk === "function") {
525
558
  callback = chunk;
@@ -575,6 +608,10 @@ function compression(options) {
575
608
  return _end.call(this, chunk, callback);
576
609
  };
577
610
 
611
+ /**
612
+ * @param {OnArgs[0]} type
613
+ * @param {OnArgs[1]} listener
614
+ */
578
615
  res.on = function on(type, listener) {
579
616
  if (!listeners || type !== "drain") {
580
617
  return _on.call(this, type, listener);
@@ -144,6 +144,7 @@ function literalValue(node) {
144
144
  return literalValue(element);
145
145
  });
146
146
  case "ObjectExpression": {
147
+ /** @type {Record<string, unknown>} */
147
148
  const out = {};
148
149
  for (const property of node.properties) {
149
150
  if (property.type !== "Property" || property.computed || property.kind !== "init") {
@@ -537,7 +538,8 @@ function readHandler(cb) {
537
538
  }
538
539
  }
539
540
 
540
- const args = fn.params.map((param) => param.name);
541
+ // undefined for a destructured one, which readParamNames reads by hand
542
+ const args = fn.params.map((/** @type {any} */ param) => param.name);
541
543
 
542
544
  if (args.length < 2) {
543
545
  return null;
@@ -607,12 +609,14 @@ function readBindings(pattern, into) {
607
609
  function readParamNames(fn, args) {
608
610
  const [req, res] = args;
609
611
  let queryName, paramsName;
610
- const queries = [],
611
- params = [];
612
+ /** @type {Binding[]} */
613
+ const queries = [];
614
+ /** @type {Binding[]} */
615
+ const params = [];
612
616
 
613
617
  if (fn.params[0].type === "ObjectPattern") {
614
- const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
615
- const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
618
+ const query = fn.params[0].properties.find((/** @type {any} */ prop) => prop.key.name === "query");
619
+ const param = fn.params[0].properties.find((/** @type {any} */ prop) => prop.key.name === "params");
616
620
 
617
621
  if (query?.value?.type === "Identifier") {
618
622
  queryName = query.value.name;
@@ -71,21 +71,21 @@ for (const member of [
71
71
  } else if (descriptor.get || descriptor.set) {
72
72
  const innerGet = descriptor.get;
73
73
  const innerSet = descriptor.set;
74
- Object.defineProperty(LazyReadableBase.prototype, member, {
75
- ...descriptor,
76
- get: innerGet
77
- ? /** @this {import("stream").Readable} */ function () {
78
- materialise(this);
79
- return innerGet.call(this);
80
- }
81
- : undefined,
82
- set: innerSet
83
- ? /** @this {import("stream").Readable} @param {unknown} value */ function (value) {
84
- materialise(this);
85
- innerSet.call(this, value);
86
- }
87
- : undefined
88
- });
74
+ // the original's accessor pair, each half wrapped where there is one
75
+ const wrapped = { ...descriptor };
76
+ if (innerGet) {
77
+ wrapped.get = /** @this {import("stream").Readable} */ function () {
78
+ materialise(this);
79
+ return innerGet.call(this);
80
+ };
81
+ }
82
+ if (innerSet) {
83
+ wrapped.set = /** @this {import("stream").Readable} @param {unknown} value */ function (value) {
84
+ materialise(this);
85
+ innerSet.call(this, value);
86
+ };
87
+ }
88
+ Object.defineProperty(LazyReadableBase.prototype, member, wrapped);
89
89
  }
90
90
  }
91
91
 
@@ -65,21 +65,21 @@ for (const member of [
65
65
  } else if (descriptor.get || descriptor.set) {
66
66
  const innerGet = descriptor.get;
67
67
  const innerSet = descriptor.set;
68
- Object.defineProperty(LazyWritableBase.prototype, member, {
69
- ...descriptor,
70
- get: innerGet
71
- ? /** @this {import("stream").Writable} */ function () {
72
- materialiseWritable(this);
73
- return innerGet.call(this);
74
- }
75
- : undefined,
76
- set: innerSet
77
- ? /** @this {import("stream").Writable} @param {unknown} value */ function (value) {
78
- materialiseWritable(this);
79
- innerSet.call(this, value);
80
- }
81
- : undefined
82
- });
68
+ // the original's accessor pair, each half wrapped where there is one
69
+ const wrapped = { ...descriptor };
70
+ if (innerGet) {
71
+ wrapped.get = /** @this {import("stream").Writable} */ function () {
72
+ materialiseWritable(this);
73
+ return innerGet.call(this);
74
+ };
75
+ }
76
+ if (innerSet) {
77
+ wrapped.set = /** @this {import("stream").Writable} @param {unknown} value */ function (value) {
78
+ materialiseWritable(this);
79
+ innerSet.call(this, value);
80
+ };
81
+ }
82
+ Object.defineProperty(LazyWritableBase.prototype, member, wrapped);
83
83
  }
84
84
  }
85
85
 
@@ -720,7 +720,7 @@ function inflateError(inflate, err) {
720
720
  *
721
721
  * @param {string|undefined} rawContentEncoding
722
722
  * @param {any} options the parser's options, read loosely: only inflate is looked at
723
- * @returns {{inflate?: Inflater, error?: HttpError}}
723
+ * @returns {{inflate?: Inflater|undefined, error?: HttpError}}
724
724
  */
725
725
  function encodingFor(rawContentEncoding, options) {
726
726
  if (!options.inflate) {
@@ -841,6 +841,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
841
841
  /** @type {string[]|null|undefined} the "body methods" setting, read on the first request */
842
842
  let additionalMethods;
843
843
 
844
+ /**
845
+ * @param {Request & {body?: unknown}} req
846
+ * @param {Response} res
847
+ * @param {(err?: unknown) => void} next
848
+ */
844
849
  const parserMiddleware = (req, res, next) => {
845
850
  // the prologue is synchronous, bindContext waits for the read (1.4us of nothing here)
846
851
 
@@ -872,8 +877,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
872
877
  }
873
878
 
874
879
  if (options.simpleType) {
875
- const semicolonIndex = type.indexOf(";");
876
- const clearType = semicolonIndex !== -1 ? type.substring(0, semicolonIndex) : type;
880
+ // only a type function lets a request without a content-type past the check above,
881
+ // and simpleType is never set beside one
882
+ const header = /** @type {string} */ (type);
883
+ const semicolonIndex = header.indexOf(";");
884
+ const clearType = semicolonIndex !== -1 ? header.substring(0, semicolonIndex) : header;
877
885
  // the trim and lowercase only when the exact compare fails
878
886
  if (clearType !== options.simpleType && clearType.trim().toLowerCase() !== options.simpleType) {
879
887
  return next();
@@ -884,7 +892,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
884
892
  return next();
885
893
  }
886
894
  } else {
887
- if (!claimsType(type)) {
895
+ if (!claimsType(/** @type {string} */ (type))) {
888
896
  return next();
889
897
  }
890
898
  }
@@ -1107,11 +1115,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1107
1115
  }
1108
1116
  }
1109
1117
  // fewer bytes than content-length promised; not when inflating, it counts the compressed ones
1110
- if (!inflate && length !== undefined && !isNaN(length) && totalSize !== Number(length)) {
1118
+ if (!inflate && !Number.isNaN(lengthNumber) && totalSize !== lengthNumber) {
1111
1119
  return next(
1112
1120
  bodyError("request size did not match content length", 400, "request.size.invalid", {
1113
- expected: Number(length),
1114
- length: Number(length),
1121
+ expected: lengthNumber,
1122
+ length: lengthNumber,
1115
1123
  received: totalSize
1116
1124
  })
1117
1125
  );
package/src/nest.js CHANGED
@@ -56,6 +56,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
56
56
  /**
57
57
  * The app is the server. Nest calls this once, from NestApplication's constructor.
58
58
  *
59
+ * @override
59
60
  * @param {import("@nestjs/common").NestApplicationOptions} [options] the options NestFactory.create was given
60
61
  * @returns {void}
61
62
  */
@@ -84,6 +85,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
84
85
  * for them in `app.router.stack`; there is no such array here, so a second call was putting a
85
86
  * second pair in front of every request.
86
87
  *
88
+ * @override
87
89
  * @param {string} [prefix]
88
90
  * @param {boolean} [rawBody]
89
91
  * @returns {void}
package/src/optimizer.js CHANGED
@@ -20,6 +20,8 @@ limitations under the License.
20
20
  /** @typedef {import("./router.js")} Router */
21
21
  /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
22
22
  /** @typedef {import("./application.js").Application} Application */
23
+ /** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
24
+ /** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
23
25
 
24
26
  const {
25
27
  patternToRegex,
@@ -44,6 +46,7 @@ const {
44
46
  hasErrorMiddleware,
45
47
  couldAnswer,
46
48
  shadowsLeaf,
49
+ headEntersGuard,
47
50
  guardsInside,
48
51
  supportedUwsMethods,
49
52
  regExParam
@@ -79,6 +82,8 @@ function optimizeRoute(router, route, routes) {
79
82
  const strictHere = (route.owner ?? router)._strictRouting();
80
83
  /** @type {string[]|null} earlier literals a case variant could smuggle a request past */
81
84
  let caseGuards = null;
85
+ // whether the HEAD twin has to stay generic, see headEnters
86
+ let headGeneric = false;
82
87
 
83
88
  for (let i = 0; i < routes.length; i++) {
84
89
  const r = routes[i];
@@ -95,6 +100,12 @@ function optimizeRoute(router, route, routes) {
95
100
  if (route.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
96
101
  return false;
97
102
  }
103
+ // Express exempts HEAD from the method check: a HEAD enters a matching route of
104
+ // any verb and its param() callbacks run, as the generic walk does. The chain
105
+ // cannot say whether such a route matches, so the HEAD twin stays generic
106
+ if (route.method === "GET" && headEnters(r, route, caseSensitive, strictHere)) {
107
+ headGeneric = true;
108
+ }
98
109
  continue;
99
110
  }
100
111
  }
@@ -115,7 +126,7 @@ function optimizeRoute(router, route, routes) {
115
126
  route.path.slice(0, matched[0].length) === matched[0] &&
116
127
  (route.path.length === matched[0].length || route.path[matched[0].length] === "/");
117
128
  if (runsAlways) {
118
- if (r.callbacks.some((c) => c instanceof Router)) {
129
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
119
130
  return false;
120
131
  }
121
132
  optimizedPath.push(r);
@@ -137,7 +148,7 @@ function optimizeRoute(router, route, routes) {
137
148
  (!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
138
149
  r.pattern === "/*"))
139
150
  ) {
140
- if (r.callbacks.some((c) => c instanceof Router)) {
151
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
141
152
  return false;
142
153
  }
143
154
  optimizedPath.push(r);
@@ -164,7 +175,7 @@ function optimizeRoute(router, route, routes) {
164
175
  }
165
176
  // the same path lands on the same µWS registration, so the earlier route runs from the chain
166
177
  if (rPathFolded === routePathFolded) {
167
- if (r.callbacks.some((c) => c instanceof Router)) {
178
+ if (r.callbacks.some((/** @type {unknown} */ c) => c instanceof Router)) {
168
179
  return false;
169
180
  }
170
181
  optimizedPath.push(r);
@@ -186,10 +197,45 @@ function optimizeRoute(router, route, routes) {
186
197
  }
187
198
  optimizedPath.push(route);
188
199
  route._caseGuards = caseGuards;
200
+ route._headGeneric = headGeneric;
189
201
 
190
202
  return optimizedPath;
191
203
  }
192
204
 
205
+ /**
206
+ * Whether a HEAD of some path this GET route answers would enter the earlier route of another
207
+ * verb for its param() callbacks: the router has callbacks and the paths can meet. A wrong yes
208
+ * costs the HEAD twin its native registration and nothing else.
209
+ *
210
+ * @param {RouteEntry} r the earlier route, of another verb
211
+ * @param {RouteEntry} route the GET route
212
+ * @param {boolean} caseSensitive
213
+ * @param {boolean} strictHere
214
+ * @returns {boolean}
215
+ */
216
+ function headEnters(r, route, caseSensitive, strictHere) {
217
+ if (r.paramCallbacks.size === 0 || typeof route.path !== "string") {
218
+ return false;
219
+ }
220
+ if (!route.path.includes(":")) {
221
+ if (typeof r.pattern === "string") {
222
+ return (
223
+ r.pattern === "/*" ||
224
+ (caseSensitive ? r.pattern === route.path : r.patternLower === route.path.toLowerCase())
225
+ );
226
+ }
227
+ return r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/"));
228
+ }
229
+ if (typeof r.path !== "string" || !canBeOptimizedWithParams(r.path)) {
230
+ return true;
231
+ }
232
+ return pathsCanOverlap(
233
+ caseSensitive ? r.path : r.path.toLowerCase(),
234
+ caseSensitive ? route.path : route.path.toLowerCase(),
235
+ r.use
236
+ );
237
+ }
238
+
193
239
  /**
194
240
  * Hands every route reachable by path alone to the native uWS router, walking into mounted
195
241
  * routers with their prefix. Runs once, at listen.
@@ -284,6 +330,16 @@ function compileOptimizedRoutes(root) {
284
330
  route._whyGeneric = "something before it in the same router overlaps its paths";
285
331
  continue;
286
332
  }
333
+ // a route of another verb before the mount that a HEAD would enter, see headEnters
334
+ if (
335
+ !route._headGeneric &&
336
+ route.method === "GET" &&
337
+ outerGuards.length > 0 &&
338
+ typeof route.path === "string" &&
339
+ outerGuards.some((g) => headEntersGuard(g, pathPrefix + route.path))
340
+ ) {
341
+ route._headGeneric = true;
342
+ }
287
343
  // an earlier parameter route in the same router would take this literal path
288
344
  if (leafPath.length > 1) {
289
345
  const shadow = leafPath[leafPath.length - 2];
@@ -360,6 +416,7 @@ function registerUwsRoute(router, route, optimizedPath) {
360
416
  * @param {import("./router-utils.js").NativePreset|undefined} preset
361
417
  * @param {{skipHeaders: boolean, skipQuery: boolean}} skips
362
418
  * @param {string|null} wireMethod
419
+ * @returns {(res: UwsResponse, req: UwsRequest) => unknown} what µWS calls
363
420
  */
364
421
  const makeHandler = (chain, preset, skips, wireMethod) => {
365
422
  // where a granted skip lives, so a middleware added after listen can take it back: the
@@ -395,10 +452,11 @@ function registerUwsRoute(router, route, optimizedPath) {
395
452
  return router._refuseRequest(response);
396
453
  }
397
454
  if (optimizedParams) {
398
- // slicing them out of the path instead measured a wash
455
+ // slicing them out of the path instead measured a wash. The index is one the
456
+ // registered path declares, so the value is never undefined
399
457
  request.optimizedParams = new NullObject();
400
458
  for (let i = 0; i < optimizedParams.length; i++) {
401
- request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
459
+ request.optimizedParams[optimizedParams[i]] = /** @type {string} */ (req.getParameter(i));
402
460
  }
403
461
  }
404
462
  const walk = new Walk(router, request, response, chain, true, skipUntil, nativeDone, nativeFail);
@@ -513,7 +571,7 @@ function registerUwsRoute(router, route, optimizedPath) {
513
571
  ? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips, wireMethod)
514
572
  : fn;
515
573
  router.uwsApp[method](replacedPath + "/", slashFn);
516
- if (method === "get") {
574
+ if (method === "get" && !route._headGeneric) {
517
575
  router.uwsApp.head(
518
576
  replacedPath + "/",
519
577
  makeHandler(
@@ -525,7 +583,7 @@ function registerUwsRoute(router, route, optimizedPath) {
525
583
  );
526
584
  }
527
585
  }
528
- if (method === "get") {
586
+ if (method === "get" && !route._headGeneric) {
529
587
  // its own handler, the shared one would carry GET
530
588
  router.uwsApp.head(
531
589
  replacedPath,
package/src/options.d.ts CHANGED
@@ -43,7 +43,7 @@ export interface SendFileOptions {
43
43
  /** Extra headers for the response. */
44
44
  headers?: Record<string, string>;
45
45
  /** Internal: express.static's setHeaders, which res.sendFile itself does not take. */
46
- _setHeaders?: (res: any, path: string, stat: any) => void;
46
+ _setHeaders?: ((res: any, path: string, stat: any) => void) | undefined;
47
47
  /** First byte of the window to send. */
48
48
  start?: number;
49
49
  /** Last byte of the window to send. */