fulmine.js 5.19.3 → 5.19.5

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.
@@ -26,6 +26,7 @@ const mime = require("mime-types");
26
26
  const compressible = require("compressible");
27
27
  const ms = require("ms");
28
28
  const qs = require("qs");
29
+ const statuses = require("statuses");
29
30
  const parseQuery = require("./parse-query.js");
30
31
  const { kGetSafe } = require("./usage.js");
31
32
  const { AsyncResource } = require("async_hooks");
@@ -48,6 +49,7 @@ const {
48
49
  NullObject,
49
50
  asStatError,
50
51
  httpError,
52
+ httpErrorName,
51
53
  memoizeByString,
52
54
  containsDotFile,
53
55
  negotiateEncoding,
@@ -74,6 +76,26 @@ const PRECOMPRESSED = [
74
76
 
75
77
  /** @typedef {import("./request.js")} Request */
76
78
  /** @typedef {import("./response.js")} Response */
79
+ /** @typedef {import("./utils.js").HttpError} HttpError */
80
+ /** @typedef {import("./options").BodyParserOptions} BodyParserOptions */
81
+ /**
82
+ * A decompressor from fast-zlib, carrying the flag its final flush passes, see createInflate.
83
+ * @typedef {(import("fast-zlib").Inflate|import("fast-zlib").Gunzip|import("fast-zlib").BrotliDecompress)
84
+ * & {_finishFlag?: number}} Inflater
85
+ */
86
+ /**
87
+ * What a parser does with the collected bytes: turns them into req.body and carries on. `body` is
88
+ * deliberately not a field of Request, see the comment there, so it is added here. The charset is
89
+ * undefined only for raw, which never decodes: the others settle it before a byte is read.
90
+ * @typedef {(
91
+ * req: Request & {body?: unknown},
92
+ * res: Response,
93
+ * next: (err?: unknown) => void,
94
+ * options: BodyParserOptions,
95
+ * buf: Buffer,
96
+ * encoding: string|undefined
97
+ * ) => void} BodyHandler
98
+ */
77
99
 
78
100
  // The failures express.static answers by moving on to the next handler rather than by reporting
79
101
  // them, when fallthrough is on. They all mean the same thing: the request is not a file here.
@@ -123,7 +145,7 @@ let iconv;
123
145
  * iconv-lite, loaded only when a request names a charset the Buffer cannot decode, so the common
124
146
  * utf-8 request never pays for it.
125
147
  *
126
- * @returns {any}
148
+ * @returns {typeof import("iconv-lite")}
127
149
  */
128
150
  function loadIconv() {
129
151
  if (!iconv) iconv = require("iconv-lite");
@@ -200,7 +222,7 @@ function decodeBody(buf, encoding) {
200
222
  case "iso-8859-1":
201
223
  return buf.toString("latin1");
202
224
  default:
203
- return loadIconv().decode(buf, encoding);
225
+ return loadIconv().decode(buf, /** @type {import("iconv-lite").Encoding} */ (encoding));
204
226
  }
205
227
  }
206
228
 
@@ -210,29 +232,51 @@ function decodeBody(buf, encoding) {
210
232
  *
211
233
  * @param {Request} req
212
234
  * @param {Response} res
213
- * @param {(err?: any) => void} next
214
- * @param {any} options the parser options, settled by createBodyParser
235
+ * @param {(err?: unknown) => void} next
236
+ * @param {BodyParserOptions} options the parser options, settled by createBodyParser
215
237
  * @param {Buffer} buf
238
+ * @param {string|undefined} encoding the charset the body is about to be decoded with, undefined
239
+ * for raw, which never decodes
216
240
  * @returns {boolean}
217
241
  */
218
- function runVerify(req, res, next, options, buf) {
242
+ function runVerify(req, res, next, options, buf, encoding) {
219
243
  if (!options.verify) {
220
244
  return true;
221
245
  }
222
246
  try {
223
- options.verify(req, res, buf);
247
+ // the charset goes too, as body-parser hands it over: a hook checking a signature over
248
+ // the decoded text needs it. raw gets null, which is what body-parser gives it there
249
+ options.verify(req, res, buf, encoding ?? null);
224
250
  return true;
225
251
  } catch (e) {
226
- const err = /** @type {any} */ (e);
227
- next(
228
- asBodyError(err, err.status ?? err.statusCode ?? 403, err.type ?? "entity.verify.failed", {
229
- body: buf
230
- })
231
- );
252
+ next(verifyError(e, buf));
232
253
  return false;
233
254
  }
234
255
  }
235
256
 
257
+ /**
258
+ * The error a verify hook's throw becomes, as http-errors shapes it for body-parser. An Error is
259
+ * kept, with its own status when that is one a client can be answered with; a thrown string
260
+ * becomes a 403 with that message, and anything else a plain 403.
261
+ *
262
+ * @param {unknown} thrown
263
+ * @param {Buffer} buf the body, which rides on the error as body-parser puts it there
264
+ * @returns {HttpError}
265
+ */
266
+ function verifyError(thrown, buf) {
267
+ const own = thrown instanceof Error ? /** @type {HttpError} */ (thrown) : undefined;
268
+ let status = own ? own.status || own.statusCode || 403 : 403;
269
+ // http-errors answers 500 for a status it cannot answer with: not a number, or outside 4xx
270
+ // and 5xx with no message of its own
271
+ if (typeof status !== "number" || (!statuses.message[status] && (status < 400 || status >= 600))) {
272
+ status = 500;
273
+ }
274
+ const err = own ?? httpError(status, typeof thrown === "string" ? thrown : undefined);
275
+ // read off whatever was thrown, as body-parser reads it
276
+ const type = /** @type {{type?: string}|null|undefined} */ (thrown)?.type || "entity.verify.failed";
277
+ return asBodyError(err, status, type, { body: buf });
278
+ }
279
+
236
280
  /**
237
281
  * The message a strict violation gets, which is the one V8 would have produced had the body been
238
282
  * invalid JSON rather than merely not an object.
@@ -255,22 +299,13 @@ function strictSyntaxMessage(text, char) {
255
299
  JSON.parse(partial);
256
300
  } catch (e) {
257
301
  // put the real characters back where the placeholders were named
258
- return /** @type {any} */ (e).message.replace(/#+/g, (/** @type {string} */ placeholder) =>
302
+ return /** @type {SyntaxError} */ (e).message.replace(/#+/g, (/** @type {string} */ placeholder) =>
259
303
  text.substring(index, index + placeholder.length)
260
304
  );
261
305
  }
262
306
  return "strict violation";
263
307
  }
264
308
 
265
- // The name http-errors gives each status body-parser answers with. An application reading
266
- // err.name, or a logger printing it, sees "PayloadTooLargeError" from Express and would have
267
- // seen a bare "Error" here.
268
- const BODY_ERROR_NAMES = {
269
- 400: "BadRequestError",
270
- 413: "PayloadTooLargeError",
271
- 415: "UnsupportedMediaTypeError"
272
- };
273
-
274
309
  /**
275
310
  * The error a body parser hands to next(), shaped as body-parser shapes it: with a status, since
276
311
  * `res.status(err.status || 500)` would otherwise answer 500 to a request that was merely too
@@ -283,10 +318,11 @@ const BODY_ERROR_NAMES = {
283
318
  * @returns {Error}
284
319
  */
285
320
  function bodyError(message, status, type, extra) {
286
- const err = /** @type {any} */ (new Error(message));
287
- if (BODY_ERROR_NAMES[status]) {
288
- err.name = BODY_ERROR_NAMES[status];
289
- }
321
+ /** @type {HttpError} */
322
+ const err = new Error(message);
323
+ // the name http-errors gives it: an application reading err.name, or a logger printing it,
324
+ // sees "PayloadTooLargeError" from Express and would have seen a bare "Error" here
325
+ err.name = httpErrorName(status);
290
326
  return asBodyError(err, status, type, extra);
291
327
  }
292
328
 
@@ -296,7 +332,7 @@ function bodyError(message, status, type, extra) {
296
332
  * its stack and any property the thrower put on it are all still there when the application
297
333
  * reads it.
298
334
  *
299
- * @param {any} err whatever was thrown, which need not be an Error
335
+ * @param {HttpError} err the error the fields go on: JSON.parse's SyntaxError, or a verify hook's own
300
336
  * @param {number} status
301
337
  * @param {string} type body-parser's own name for the kind of failure
302
338
  * @param {object} [extra] anything else body-parser puts on that particular error
@@ -411,7 +447,7 @@ function pickPrecompressed(filePath, accept, ttl, statTtl) {
411
447
  *
412
448
  * @param {string} dir the directory to look in
413
449
  * @param {string[]} indexList the index names, in order
414
- * @returns {{stat: any, name: string, candidate: string}|null} the file to serve, or null
450
+ * @returns {{stat: import("fs").Stats, name: string, candidate: string}|null} the file to serve, or null
415
451
  */
416
452
  function findIndexFile(dir, indexList) {
417
453
  let lastError;
@@ -445,7 +481,7 @@ function findIndexFile(dir, indexList) {
445
481
  *
446
482
  * @param {string} root directory to serve from
447
483
  * @param {import("./options").StaticOptions} [options]
448
- * @returns {(req: any, res: any, next: (err?: any) => void) => any}
484
+ * @returns {(req: Request, res: Response, next: (err?: unknown) => void) => void}
449
485
  */
450
486
  function serveStatic(root, options) {
451
487
  // serve-static's own messages, thrown where the middleware is written rather than where a
@@ -478,21 +514,22 @@ function serveStatic(root, options) {
478
514
  if (options.setHeaders !== undefined && typeof options.setHeaders !== "function") {
479
515
  throw new TypeError("option setHeaders must be function");
480
516
  }
517
+ // serve-static's own option, which res.sendFile does not take, so it goes down under a name
518
+ // only this middleware writes, see sendFile
519
+ options._setHeaders = options.setHeaders;
481
520
  // How long express.static remembers which twins a path has. A second is short enough that a
482
521
  // deploy is picked up while it is still going out, and long enough that the lookup costs
483
522
  // nothing under any traffic at all. { cache: false } asks the disk on every request.
484
523
  let twinTtl = 0;
485
524
  if (options.preCompressed) {
486
- const cache = /** @type {any} */ (
487
- typeof options.preCompressed === "object" ? options.preCompressed.cache : undefined
488
- );
525
+ const cache = typeof options.preCompressed === "object" ? options.preCompressed.cache : undefined;
489
526
  twinTtl =
490
527
  cache === undefined
491
528
  ? 1000
492
529
  : cache === false
493
530
  ? 0
494
531
  : typeof cache === "string"
495
- ? ms(/** @type {any} */ (cache))
532
+ ? ms(/** @type {import("ms").StringValue} */ (cache))
496
533
  : cache;
497
534
  if (typeof twinTtl !== "number" || !(twinTtl >= 0)) {
498
535
  throw new TypeError("option preCompressed.cache must be a duration");
@@ -662,7 +699,7 @@ function serveStatic(root, options) {
662
699
  // error handler with its errno, code, syscall and path still on it, and an
663
700
  // error handler doing res.send(err) sends those as JSON. Passing the string
664
701
  // sent an HTML page instead.
665
- return next(asStatError(statError));
702
+ return next(asStatError(/** @type {HttpError} */ (statError)));
666
703
  } else return next();
667
704
  }
668
705
  }
@@ -707,7 +744,7 @@ function serveStatic(root, options) {
707
744
  res.status(404);
708
745
  // the fs error, as above: the index file is missing and the error handler
709
746
  // is told which one and where
710
- return next(asStatError(err));
747
+ return next(asStatError(/** @type {HttpError} */ (err)));
711
748
  } else return next();
712
749
  }
713
750
  if (found === null) {
@@ -773,12 +810,59 @@ function serveStatic(root, options) {
773
810
  };
774
811
  }
775
812
 
813
+ /**
814
+ * A zlib throw as the 400 body-parser answers a corrupt body with. zlib reports it twice, and the
815
+ * 'error' a tick later would land on nothing and end the process, so the listener goes back on.
816
+ *
817
+ * @param {Inflater} inflate
818
+ * @param {HttpError} err what inflate.process threw
819
+ * @returns {HttpError} the same error, carrying its status
820
+ */
821
+ function inflateError(inflate, err) {
822
+ inflate.instance?.on?.("error", () => {});
823
+ err.status = 400;
824
+ err.statusCode = 400;
825
+ err.expose = true;
826
+ return err;
827
+ }
828
+
829
+ /**
830
+ * What a Content-Encoding means here: the decompressor to run, or the 415 it is refused with. An
831
+ * empty body is judged the same way, so both callers share this.
832
+ *
833
+ * @param {string|undefined} rawContentEncoding
834
+ * @param {any} options the parser's options, read loosely: only inflate is looked at
835
+ * @returns {{inflate?: Inflater, error?: HttpError}}
836
+ */
837
+ function encodingFor(rawContentEncoding, options) {
838
+ if (!options.inflate) {
839
+ const contentEncoding = (rawContentEncoding || "identity").toLowerCase();
840
+ if (contentEncoding !== "identity") {
841
+ return {
842
+ error: bodyError("content encoding unsupported", 415, "encoding.unsupported", {
843
+ encoding: contentEncoding
844
+ })
845
+ };
846
+ }
847
+ return {};
848
+ }
849
+ const inflate = createInflate(rawContentEncoding);
850
+ if (inflate === false) {
851
+ return {
852
+ error: bodyError('unsupported content encoding "' + rawContentEncoding + '"', 415, "encoding.unsupported", {
853
+ encoding: rawContentEncoding
854
+ })
855
+ };
856
+ }
857
+ return { inflate };
858
+ }
859
+
776
860
  /**
777
861
  * The decompressor for a Content-Encoding, or undefined when the body is not compressed. An
778
862
  * encoding nobody knows throws, since decoding it wrong is worse than refusing.
779
863
  *
780
864
  * @param {string|undefined} contentEncoding
781
- * @returns {any|undefined}
865
+ * @returns {Inflater|false|undefined}
782
866
  */
783
867
  function createInflate(contentEncoding) {
784
868
  const encoding = (contentEncoding || "identity").toLowerCase();
@@ -799,7 +883,7 @@ function createInflate(contentEncoding) {
799
883
  return false;
800
884
  }
801
885
  // the flag the final flush passes, so a truncated stream errors instead of resolving empty
802
- /** @type {any} */ (stream)._finishFlag =
886
+ /** @type {Inflater} */ (stream)._finishFlag =
803
887
  encoding === "br" ? zlib.constants.BROTLI_OPERATION_FINISH : zlib.constants.Z_FINISH;
804
888
  return stream;
805
889
  }
@@ -810,9 +894,9 @@ function createInflate(contentEncoding) {
810
894
  * bytes over. They differ in the content type they claim and in what they turn the bytes into.
811
895
  *
812
896
  * @param {string} defaultType the type matched when the caller names none
813
- * @param {(...args: any[]) => any} beforeReturn turns the collected bytes into req.body. Called
814
- * with the request, the response, next, the options, the body and its charset
815
- * @param {(options: any) => void} [checkOptions] whatever this parser alone has to check
897
+ * @param {BodyHandler} beforeReturn turns the collected bytes into req.body. Called with the
898
+ * request, the response, next, the options, the body and its charset
899
+ * @param {(options: BodyParserOptions) => void} [checkOptions] whatever this parser alone has to check
816
900
  * @param {string} [charsetPolicy] which charsets this parser accepts, as body-parser draws the
817
901
  * lines: "utf" (json, utf-* only), "urlencoded" (utf-8 and iso-8859-1), "any" (anything iconv
818
902
  * knows), or undefined for a parser that never decodes (raw)
@@ -949,8 +1033,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
949
1033
  }
950
1034
  }
951
1035
 
952
- // the charset is settled before anything is read, as body-parser settles it: a bad one
953
- // answers 415 even for an empty body, and before the verify hook can run
1036
+ // The charset is settled before anything is read, as body-parser settles it: a bad one
1037
+ // answers 415 even for an empty body, and before the verify hook can run. Its two
1038
+ // halves sit on either side of the encoding, which is the order body-parser reads
1039
+ // them in: the charset this parser accepts at all, then the Content-Encoding, then
1040
+ // whether iconv knows the charset.
954
1041
  let encoding;
955
1042
  if (charsetPolicy) {
956
1043
  encoding = charsetOf(type) ?? defaultCharset;
@@ -960,9 +1047,16 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
960
1047
  ) {
961
1048
  return next(charsetError(encoding));
962
1049
  }
963
- if (!BUFFER_CHARSETS.has(encoding) && !loadIconv().encodingExists(encoding)) {
964
- return next(charsetError(encoding));
965
- }
1050
+ }
1051
+
1052
+ const encoded = encodingFor(req._rawHeader("content-encoding"), options);
1053
+ if (encoded.error) {
1054
+ return next(encoded.error);
1055
+ }
1056
+ const inflate = encoded.inflate;
1057
+
1058
+ if (encoding !== undefined && !BUFFER_CHARSETS.has(encoding) && !loadIconv().encodingExists(encoding)) {
1059
+ return next(charsetError(encoding));
966
1060
  }
967
1061
 
968
1062
  // an empty body still has to produce this parser's empty value the way express does -
@@ -971,8 +1065,18 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
971
1065
  // and the verify hook still runs first: webhook signature checks rely on that
972
1066
  if (lengthNumber === 0) {
973
1067
  req.bodyRead = true;
974
- const empty = Buffer.alloc(0);
975
- if (!runVerify(req, res, next, options, empty)) {
1068
+ /** @type {Buffer<ArrayBufferLike>} what the parser is handed: zlib's tail is wider */
1069
+ let empty = Buffer.alloc(0);
1070
+ if (inflate) {
1071
+ // nothing to inflate is a stream cut short for zlib, and body-parser answers
1072
+ // that with a 400 rather than with the empty value
1073
+ try {
1074
+ empty = inflate.process(EMPTY_BUFFER, inflate._finishFlag);
1075
+ } catch (e) {
1076
+ return next(inflateError(inflate, /** @type {HttpError} */ (e)));
1077
+ }
1078
+ }
1079
+ if (!runVerify(req, res, next, options, empty, encoding)) {
976
1080
  return;
977
1081
  }
978
1082
  return beforeReturn(req, res, next, options, empty, encoding);
@@ -1002,32 +1106,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1002
1106
  return next();
1003
1107
  }
1004
1108
 
1005
- let inflate;
1006
1109
  let totalSize = 0;
1007
- const rawContentEncoding = req._rawHeader("content-encoding");
1008
- const contentEncoding = (rawContentEncoding || "identity").toLowerCase();
1009
- if (!options.inflate && contentEncoding !== "identity") {
1010
- return next(
1011
- bodyError("content encoding unsupported", 415, "encoding.unsupported", {
1012
- encoding: contentEncoding
1013
- })
1014
- );
1015
- }
1016
- if (options.inflate) {
1017
- inflate = createInflate(rawContentEncoding);
1018
- if (inflate === false) {
1019
- return next(
1020
- bodyError(
1021
- 'unsupported content encoding "' + rawContentEncoding + '"',
1022
- 415,
1023
- "encoding.unsupported",
1024
- {
1025
- encoding: rawContentEncoding
1026
- }
1027
- )
1028
- );
1029
- }
1030
- }
1031
1110
 
1032
1111
  // From here the body really gets read, and uWS delivers it on native callbacks that
1033
1112
  // carry no async context, so this is the one continuation that has to be bound: an
@@ -1070,7 +1149,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1070
1149
  if (copyBody) {
1071
1150
  buf = Buffer.from(buf);
1072
1151
  }
1073
- if (!runVerify(req, res, next, options, buf)) {
1152
+ if (!runVerify(req, res, next, options, buf, encoding)) {
1074
1153
  return;
1075
1154
  }
1076
1155
  beforeReturn(req, res, next, options, buf, encoding);
@@ -1099,24 +1178,16 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1099
1178
  let finished = false;
1100
1179
 
1101
1180
  /**
1102
- * A zlib throw becomes the 400 body-parser answers a corrupt body with.
1103
- *
1104
- * zlib reports it twice: process() throws, and the stream emits 'error' a tick later.
1105
- * fast-zlib removes its own listeners on the way out, so that second one lands on
1106
- * nothing, and an unhandled 'error' event ends the process. The listener goes on after
1107
- * the throw, since process() would have removed it.
1181
+ * A zlib throw becomes the 400 body-parser answers a corrupt body with, and what was
1182
+ * kept of the body goes.
1108
1183
  *
1109
- * @param {any} err what inflate.process threw
1184
+ * @param {HttpError} err what inflate.process threw
1110
1185
  */
1111
1186
  function failInflate(err) {
1112
- /** @type {any} */ (inflate).instance?.on?.("error", () => {});
1113
1187
  finished = true;
1114
1188
  abs.length = 0;
1115
1189
  target = null;
1116
- err.status = 400;
1117
- err.statusCode = 400;
1118
- err.expose = true;
1119
- next(err);
1190
+ next(inflateError(/** @type {Inflater} */ (inflate), err));
1120
1191
  }
1121
1192
 
1122
1193
  /**
@@ -1162,7 +1233,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1162
1233
  * finished flag matters: uWS goes on delivering chunks after an oversized body has
1163
1234
  * been refused, and without it every further chunk would answer the request again.
1164
1235
  *
1165
- * @param {any} buf a Buffer, or an ArrayBuffer straight from uWS
1236
+ * @param {Buffer|ArrayBuffer} buf a Buffer, or an ArrayBuffer straight from uWS
1166
1237
  */
1167
1238
  function onData(buf) {
1168
1239
  if (finished) {
@@ -1177,7 +1248,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1177
1248
  } catch (e) {
1178
1249
  // a body that does not decompress is the client's mistake, and zlib
1179
1250
  // throwing here used to escape into whatever called us
1180
- return failInflate(e);
1251
+ return failInflate(/** @type {HttpError} */ (e));
1181
1252
  }
1182
1253
  }
1183
1254
 
@@ -1198,7 +1269,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1198
1269
  try {
1199
1270
  tail = inflate.process(EMPTY_BUFFER, inflate._finishFlag);
1200
1271
  } catch (e) {
1201
- return failInflate(e);
1272
+ return failInflate(/** @type {HttpError} */ (e));
1202
1273
  }
1203
1274
  if (tail.length && !keepChunk(tail)) {
1204
1275
  return;
@@ -1226,7 +1297,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1226
1297
  : abs.length === 1
1227
1298
  ? abs[0]
1228
1299
  : Buffer.concat(abs);
1229
- if (!runVerify(req, res, next, options, buf)) {
1300
+ if (!runVerify(req, res, next, options, buf, encoding)) {
1230
1301
  return;
1231
1302
  }
1232
1303
  beforeReturn(req, res, next, options, buf, encoding);
@@ -1274,7 +1345,7 @@ const json = createBodyParser(
1274
1345
  // either: it decodes through iconv, which strips it, so by the time the first character is
1275
1346
  // looked at the mark is gone. JSON.parse would refuse it, so without this a body saved by
1276
1347
  // an editor that writes a BOM is answered 400 here and 200 by Express.
1277
- const text = stripBom(decodeBody(buf, encoding));
1348
+ const text = stripBom(decodeBody(buf, /** @type {string} */ (encoding)));
1278
1349
 
1279
1350
  // "strict" means only an object or an array is a body, which is body-parser's default and
1280
1351
  // was not honoured here at all: the check read req.body before this function had parsed
@@ -1302,7 +1373,7 @@ const json = createBodyParser(
1302
1373
  } catch (e) {
1303
1374
  // V8's own error, which is what body-parser hands on: its message says where the parse
1304
1375
  // gave up, and it is still the SyntaxError an application may be testing for
1305
- const err = /** @type {any} */ (e);
1376
+ const err = /** @type {SyntaxError} */ (e);
1306
1377
  return next(asBodyError(err, 400, "entity.parse.failed", { body: text }));
1307
1378
  }
1308
1379
 
@@ -1329,7 +1400,7 @@ const text = createBodyParser(
1329
1400
  "text/plain",
1330
1401
  function (req, res, next, options, buf, encoding) {
1331
1402
  try {
1332
- req.body = decodeBody(buf, encoding);
1403
+ req.body = decodeBody(buf, /** @type {string} */ (encoding));
1333
1404
  } catch (e) {
1334
1405
  return next(e);
1335
1406
  }
@@ -1371,7 +1442,7 @@ const urlencoded = createBodyParser(
1371
1442
  "application/x-www-form-urlencoded",
1372
1443
  function (req, res, next, options, buf, encoding) {
1373
1444
  try {
1374
- const body = decodeBody(buf, encoding);
1445
+ const body = decodeBody(buf, /** @type {string} */ (encoding));
1375
1446
  // Express 5 defaults extended to false, so nested keys need opting in
1376
1447
  const extended = typeof options.extended !== "undefined" ? options.extended : false;
1377
1448
  // qs has to know the charset itself for anything but utf-8, and the sentinel options
@@ -1387,7 +1458,8 @@ const urlencoded = createBodyParser(
1387
1458
  }
1388
1459
  req.body = parsed;
1389
1460
  } else {
1390
- const count = parameterCount(body, options.parameterLimit);
1461
+ // settled by the check below the parser, so it is a number by the time a body arrives
1462
+ const count = parameterCount(body, /** @type {number} */ (options.parameterLimit));
1391
1463
  if (count === undefined) {
1392
1464
  return next(bodyError("too many parameters", 413, "parameters.too.many"));
1393
1465
  }
@@ -1401,7 +1473,7 @@ const urlencoded = createBodyParser(
1401
1473
  arrayLimit: Math.max(100, count + 1),
1402
1474
  charsetSentinel: options.charsetSentinel,
1403
1475
  interpretNumericEntities: options.interpretNumericEntities,
1404
- charset: encoding,
1476
+ charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
1405
1477
  parameterLimit: options.parameterLimit
1406
1478
  };
1407
1479
  req.body = needsQs
@@ -1419,7 +1491,7 @@ const urlencoded = createBodyParser(
1419
1491
  strictDepth: true,
1420
1492
  charsetSentinel: options.charsetSentinel,
1421
1493
  interpretNumericEntities: options.interpretNumericEntities,
1422
- charset: encoding,
1494
+ charset: /** @type {"utf-8"|"iso-8859-1"} */ (encoding),
1423
1495
  parameterLimit: options.parameterLimit
1424
1496
  })
1425
1497
  );
package/src/nest.js CHANGED
@@ -47,7 +47,8 @@ const fulmine = require("./index.js");
47
47
  */
48
48
  class FulmineExpressAdapter extends ExpressAdapter {
49
49
  /**
50
- * @param {any} [instance] an application from `fulmine()`; one is created when omitted
50
+ * @param {import("fulmine.js").FulmineApplication} [instance] an application from `fulmine()`; one is
51
+ * created when omitted
51
52
  */
52
53
  constructor(instance) {
53
54
  super(instance || fulmine());
@@ -61,7 +62,7 @@ class FulmineExpressAdapter extends ExpressAdapter {
61
62
  /**
62
63
  * The app is the server. Nest calls this once, from NestApplication's constructor.
63
64
  *
64
- * @param {any} [options] the options NestFactory.create was given
65
+ * @param {import("@nestjs/common").NestApplicationOptions} [options] the options NestFactory.create was given
65
66
  * @returns {void}
66
67
  */
67
68
  initHttpServer(options) {
package/src/node-shim.js CHANGED
@@ -389,7 +389,7 @@ class NodeHttpResponse {
389
389
 
390
390
  /**
391
391
  * Whether these are node's own request and response rather than this project's.
392
- * @param {any} req anything a caller handed the router, which is the point of the check
392
+ * @param {unknown} req anything a caller handed the router, which is the point of the check
393
393
  */
394
394
  function isNodeRequest(req) {
395
395
  return req instanceof IncomingMessage;
@@ -398,14 +398,19 @@ function isNodeRequest(req) {
398
398
  /**
399
399
  * Serves a request that arrived through node's HTTP server with the given router or app.
400
400
  *
401
- * @param {any} router the router or application serving this request
401
+ * @param {import("./router.js")} router the router or application serving this request
402
402
  * @param {import("http").IncomingMessage} nodeReq
403
403
  * @param {import("http").ServerResponse} nodeRes
404
- * @param {(err?: any) => void} [next] called when nothing in the router answered
404
+ * @param {(err?: unknown) => void} [next] called when nothing in the router answered
405
405
  */
406
406
  function serveNodeRequest(router, nodeReq, nodeRes, next) {
407
- const shimRes = new NodeHttpResponse(nodeReq, nodeRes);
408
- const shimReq = new NodeHttpRequest(nodeReq);
407
+ // the shims stand in for uWS's pair, and the checker is told so once, here
408
+ const shimRes = /** @type {import("uWebSockets.js").HttpResponse} */ (
409
+ /** @type {unknown} */ (new NodeHttpResponse(nodeReq, nodeRes))
410
+ );
411
+ const shimReq = /** @type {import("uWebSockets.js").HttpRequest} */ (
412
+ /** @type {unknown} */ (new NodeHttpRequest(nodeReq))
413
+ );
409
414
  const request = router.handleRequest(shimRes, shimReq);
410
415
  const response = request.res;
411
416
  // the shim's onAborted rides node's own close event, needed on every request here
package/src/optimizer.js CHANGED
@@ -19,6 +19,7 @@ limitations under the License.
19
19
 
20
20
  /** @typedef {import("./router.js")} Router */
21
21
  /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
22
+ /** @typedef {import("./application.js").Application} Application */
22
23
 
23
24
  const {
24
25
  patternToRegex,
@@ -53,7 +54,7 @@ const {
53
54
  let Router;
54
55
 
55
56
  /**
56
- * @param {any} cls the Router class, passed in to keep this module out of its require cycle
57
+ * @param {typeof import("./router.js")} cls the Router class, passed in to keep this module out of its require cycle
57
58
  */
58
59
  function useRouterClass(cls) {
59
60
  Router = cls;
@@ -66,8 +67,8 @@ function useRouterClass(cls) {
66
67
  *
67
68
  * @param {Router} router
68
69
  * @param {RouteEntry} route
69
- * @param {any[]} routes every route of this router, in registration order
70
- * @returns {any[]|false} the chain, ending in the route itself
70
+ * @param {RouteEntry[]} routes every route of this router, in registration order
71
+ * @returns {RouteEntry[]|false} the chain, ending in the route itself
71
72
  */
72
73
  function optimizeRoute(router, route, routes) {
73
74
  const optimizedPath = [];
@@ -216,7 +217,8 @@ function optimizeRoute(router, route, routes) {
216
217
  * routers and carrying their prefix down. Runs once, when the app starts listening, since it
217
218
  * needs every route to have been registered first.
218
219
  *
219
- * @param {any} root the application whose routes are being compiled
220
+ * @param {Router} root the application whose routes are being compiled. A plain router, which has
221
+ * no uwsApp, is left alone
220
222
  */
221
223
  function compileOptimizedRoutes(root) {
222
224
  if (!root.uwsApp) {
@@ -376,7 +378,7 @@ function compileOptimizedRoutes(root) {
376
378
  *
377
379
  * @param {Router} router
378
380
  * @param {RouteEntry} route
379
- * @param {any[]} optimizedPath the routes to run, in order, ending with this one
381
+ * @param {RouteEntry[]} optimizedPath the routes to run, in order, ending with this one
380
382
  */
381
383
  function registerUwsRoute(router, route, optimizedPath) {
382
384
  let method = route.method.toLowerCase();
@@ -428,7 +430,7 @@ function registerUwsRoute(router, route, optimizedPath) {
428
430
  // this one
429
431
  if (caseGuards !== null && anyGuardHits(caseGuards, req.getUrl())) {
430
432
  // an application is what registers native routes, and only it serves
431
- return /** @type {any} */ (router)._serveGeneric(res, req);
433
+ return /** @type {Application} */ (router)._serveGeneric(res, req);
432
434
  }
433
435
  const request = router.handleRequest(res, req, preset, skipHolder);
434
436
  const response = request.res;
@@ -526,7 +528,7 @@ function registerUwsRoute(router, route, optimizedPath) {
526
528
 
527
529
  // the response prototype the route will really run under: its own app's, which sees a
528
530
  // method patched there or inherited from a parent app, falling back to the registering app
529
- const responseProto = /** @type {any} */ (route.owner)?.response ?? /** @type {any} */ (router).response;
531
+ const responseProto = route.owner?.response ?? /** @type {Application} */ (router).response;
530
532
  // check if route is declarative
531
533
  if (
532
534
  optimizedPath.length === 1 && // must not have middlewares