fulmine.js 5.19.3 → 5.19.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/src/response.js CHANGED
@@ -38,6 +38,8 @@ const {
38
38
  withUtf8Charset,
39
39
  asStatError,
40
40
  httpError,
41
+ headersSentError,
42
+ applyWriteHead,
41
43
  contentTypeFor,
42
44
  statTag,
43
45
  cachedStat,
@@ -79,6 +81,9 @@ const JSON_UTF8 = "application/json; charset=utf-8";
79
81
  // than written out, since a year is already longer than any cache will honour.
80
82
  const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
81
83
 
84
+ // what send takes as a range request: the bytes unit, checked on the header's text before parsing
85
+ const BYTES_RANGE = /^ *bytes=/;
86
+
82
87
  module.exports = class Response extends LazyWritable {
83
88
  /** @type {Socket|null} */
84
89
  #socket = null;
@@ -106,15 +111,21 @@ module.exports = class Response extends LazyWritable {
106
111
  /** Whether a flush is already booked for the end of this turn. */
107
112
  #flushBooked = false;
108
113
 
109
- /** @type {any} */
110
- #outHeaders = null;
114
+ /** Whether the status line and the headers have reached uWS, which only a body write does. */
115
+ #headOut = false;
111
116
 
112
117
  /**
113
- * Whether node's writeHead has run, which only the per-app subclass sets. _sendOptionsReply
114
- * refuses to write a second head over it, as node's setHeader does.
115
- * @type {boolean|undefined}
118
+ * The status line as writeHead settled it, which is the one the wire gets: node stores the
119
+ * head at writeHead, so a status set later never reaches the client.
120
+ * @type {number}
116
121
  */
117
- _headWritten;
122
+ #status = 200;
123
+
124
+ /** @type {string|undefined} */
125
+ #statusText = undefined;
126
+
127
+ /** @type {Response["headers"]|null} */
128
+ #outHeaders = null;
118
129
 
119
130
  /**
120
131
  * The request this response answers, linked so either reaches the other.
@@ -127,10 +138,9 @@ module.exports = class Response extends LazyWritable {
127
138
  * two that describe the connection, since every response carries them, and x-powered-by only
128
139
  * when the setting asks for it.
129
140
  *
130
- * @param {any} res the uWS response
131
- * @param {any} req the Request, already built. Loose because the per-app subclass in
132
- * application.js inherits this constructor and its own shape has to stay assignable
133
- * @param {any} app the application or router this request arrived at
141
+ * @param {import("uWebSockets.js").HttpResponse} res the uWS response
142
+ * @param {InstanceType<typeof import("./request.js")>} req the Request, already built
143
+ * @param {import("./application.js").Application} app the application this request arrived at
134
144
  */
135
145
  constructor(res, req, app) {
136
146
  super();
@@ -139,6 +149,7 @@ module.exports = class Response extends LazyWritable {
139
149
  // the order node's own Writable constructor lays down, so the hidden class is the one every
140
150
  // other stream in the process has, and node's init keeps this object when the state is
141
151
  // finally built
152
+ /** @type {Record<string, Function|undefined>} */
142
153
  this._events = {
143
154
  close: undefined,
144
155
  error: undefined,
@@ -191,7 +202,7 @@ module.exports = class Response extends LazyWritable {
191
202
  this.body = undefined;
192
203
  // what was handed to uWS, kept so a caller asking for content-length after the fact can be
193
204
  // answered, see get(). Undefined until the response ends, and for one that sends no body
194
- /** @type {string|Buffer|undefined} */
205
+ /** @type {string|Buffer|Uint8Array|undefined} */
195
206
  this._sentBody = undefined;
196
207
  // false while the uWS route handler is still in its synchronous window, where uWS holds
197
208
  // the socket corked itself; the two uWS entry points flip it once that window closes
@@ -206,8 +217,7 @@ module.exports = class Response extends LazyWritable {
206
217
  //
207
218
  // The condition is the safety of it: a fresh response has no listeners, so both slots are
208
219
  // free, and anything else falls back to on(), which finds what this wrote.
209
- // cast because _events and _eventsCount are EventEmitter's own bookkeeping and have no type
210
- const self = /** @type {any} */ (this);
220
+ const self = this;
211
221
  const events = self._events;
212
222
  if (
213
223
  self._eventsCount === 0 &&
@@ -252,7 +262,7 @@ module.exports = class Response extends LazyWritable {
252
262
  * aborted uWS response is a use after free. writableEnded reads true after this, where node
253
263
  * leaves it false until end() is called.
254
264
  *
255
- * @param {any} [error] whatever the caller is destroying the response with
265
+ * @param {Error} [error] whatever the caller is destroying the response with
256
266
  * @returns {this}
257
267
  */
258
268
  destroy(error) {
@@ -282,7 +292,7 @@ module.exports = class Response extends LazyWritable {
282
292
  return;
283
293
  }
284
294
  this._pendingLinked = false;
285
- const pending = /** @type {any} */ (this)._pendingIn;
295
+ const pending = /** @type {{_pendingIn?: {head: Response|null}}} */ (this)._pendingIn;
286
296
  const prev = this._pendingPrev;
287
297
  const next = this._pendingNext;
288
298
  if (prev) {
@@ -337,7 +347,7 @@ module.exports = class Response extends LazyWritable {
337
347
  * over, as node's does; the request's `socket` is the same object and stays, so it comes
338
348
  * through here instead.
339
349
  *
340
- * @returns {any}
350
+ * @returns {Socket}
341
351
  */
342
352
  _socketShim() {
343
353
  if (!this.#socket) {
@@ -428,13 +438,15 @@ module.exports = class Response extends LazyWritable {
428
438
 
429
439
  this.writingChunk = true;
430
440
  this._res.cork(() => {
431
- if (!this.headersSent) {
432
- this.writeHead(this.statusCode);
441
+ if (!this.#headOut) {
442
+ if (!this.headersSent) {
443
+ this.writeHead(this.statusCode);
444
+ }
433
445
  // "unknown" and not the bare number: node writes that reason phrase for a code it
434
446
  // has no message for, so the raw status lines match. The default 200 with no
435
447
  // phrase is uWS's own head, byte for byte, so it is not written at all
436
- if (this.statusCode !== 200 || this.statusText !== undefined) {
437
- this._res.writeStatus(statusLine(this.statusCode, this.statusText));
448
+ if (this.#status !== 200 || this.#statusText !== undefined) {
449
+ this._res.writeStatus(statusLine(this.#status, this.#statusText));
438
450
  }
439
451
  this.writeHeaders(typeof chunk === "string");
440
452
  }
@@ -503,8 +515,9 @@ module.exports = class Response extends LazyWritable {
503
515
 
504
516
  /**
505
517
  * Sets the status and, optionally, a batch of headers, the way node does. The second argument
506
- * is either the status message or the headers, since node allows both shapes. Nothing is
507
- * written here despite the name: the headers go out when the body does.
518
+ * is either the status message or the headers, since node allows both shapes. The bytes go out
519
+ * with the body, but the head is settled here, as node's is: headersSent reads true from now
520
+ * on, what is set later throws, and a status written later never reaches the wire.
508
521
  *
509
522
  * Every header goes through setHeader and not through set. This is node's method, not
510
523
  * Express's: a content-type given here keeps the value it was given, where res.set would append
@@ -512,38 +525,23 @@ module.exports = class Response extends LazyWritable {
512
525
  * @sveltejs/adapter-node included, so the charset was added to pages nobody asked it for.
513
526
  *
514
527
  * @param {number} statusCode
515
- * @param {string|Record<string, any>|any[]} [statusMessage] the reason phrase, or the headers
516
- * @param {Record<string, any>|any[]} [headers]
528
+ * @param {string|import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [statusMessage] the
529
+ * reason phrase, or the headers
530
+ * @param {import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [headers]
517
531
  * @returns {this}
518
532
  */
519
533
  writeHead(statusCode, statusMessage, headers) {
520
- this.statusCode = statusCode;
521
- if (typeof statusMessage === "string") {
522
- this.statusText = statusMessage;
523
- }
524
- if (!headers) {
525
- if (!statusMessage) return this;
526
- // the two-argument shape, where what looked like a reason phrase is the headers. A
527
- // string reaching here was already taken as the phrase above and simply has no keys.
528
- headers = /** @type {Record<string, any>} */ (statusMessage);
529
- }
530
- if (Array.isArray(headers)) {
531
- // node takes a flat list here, name then value, and not a list of pairs. An odd length
532
- // is the caller's mistake and node names the argument in what it throws
533
- if (headers.length % 2 !== 0) {
534
- /** @type {NodeJS.ErrnoException} */
535
- const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
536
- err.code = "ERR_INVALID_ARG_VALUE";
537
- throw err;
538
- }
539
- for (let i = 0; i < headers.length; i += 2) {
540
- this.setHeader(headers[i], headers[i + 1]);
541
- }
542
- return this;
534
+ if (this.headersSent) {
535
+ throw headersSentError("write");
543
536
  }
544
- for (const header in headers) {
545
- this.setHeader(header, headers[header]);
537
+ this.statusCode = statusCode;
538
+ const reason = applyWriteHead(this, statusMessage, headers);
539
+ if (reason !== undefined) {
540
+ this.statusText = reason;
546
541
  }
542
+ this.#status = statusCode;
543
+ this.#statusText = this.statusText;
544
+ this.headersSent = true;
547
545
  return this;
548
546
  }
549
547
 
@@ -597,16 +595,19 @@ module.exports = class Response extends LazyWritable {
597
595
  }
598
596
  }
599
597
  this.headersSent = true;
598
+ this.#headOut = true;
600
599
  }
601
600
 
602
601
  /**
603
- * What node calls before writing a body when the caller never called writeHead. Here there is
604
- * nothing to flush, since the headers are written with the body, so this only fixes the status.
602
+ * What node calls before writing a body when the caller never called writeHead: it settles the
603
+ * head, and nothing is flushed, since the headers are written with the body. Not once the head
604
+ * is settled: the compression module calls this on the strength of node's _header, which this
605
+ * response does not keep, so the guard is here instead of there.
605
606
  */
606
607
  _implicitHeader() {
607
- // compatibility function
608
- // usually should send headers but this is useless for us
609
- this.writeHead(this.statusCode);
608
+ if (!this.headersSent) {
609
+ this.writeHead(this.statusCode);
610
+ }
610
611
  }
611
612
 
612
613
  /**
@@ -645,17 +646,29 @@ module.exports = class Response extends LazyWritable {
645
646
  }
646
647
 
647
648
  /**
648
- * @param {any} [data] the last body piece, or the callback in node's two-argument shape
649
- * @param {any} [cb]
649
+ * @param {string|Buffer|Uint8Array|null|(() => void)} [data] the last body piece, or the callback in
650
+ * node's one-argument shape
651
+ * @param {BufferEncoding|(() => void)} [encoding] how a string body is encoded, or the callback in
652
+ * node's two-argument shape
653
+ * @param {() => void} [cb]
650
654
  * @returns {this}
651
655
  */
652
- end(data, cb) {
656
+ end(data, encoding, cb) {
653
657
  if (typeof data === "function") {
654
658
  cb = data;
655
659
  data = undefined;
660
+ encoding = undefined;
661
+ } else if (typeof encoding === "function") {
662
+ cb = encoding;
663
+ encoding = undefined;
656
664
  }
657
665
  if (typeof cb !== "function") {
658
- cb = undefined; // silence the error?
666
+ cb = undefined;
667
+ }
668
+ // uWS takes a string as utf-8 and nothing else, so any other encoding is applied here, the
669
+ // way write() applies it: res.end(data, "binary") is how old code sends an image
670
+ if (typeof data === "string" && encoding !== undefined && encoding !== "utf8" && encoding !== "utf-8") {
671
+ data = Buffer.from(data, encoding);
659
672
  }
660
673
 
661
674
  if (this.writingChunk) {
@@ -667,7 +680,11 @@ module.exports = class Response extends LazyWritable {
667
680
  if (this.finished) {
668
681
  return this;
669
682
  }
670
- this.writeHead(this.statusCode);
683
+ // as node's end() calls _implicitHeader: not after an explicit writeHead, which settled the
684
+ // head already and told on-headers' listeners
685
+ if (!this.headersSent) {
686
+ this.writeHead(this.statusCode);
687
+ }
671
688
  // uWS holds the socket corked for the synchronous window of its route handler, and
672
689
  // cork inside cork is a passthrough: the wrapper and its closure are only paid once the
673
690
  // answer has outlived that window, which is what _corkNeeded records
@@ -683,23 +700,23 @@ module.exports = class Response extends LazyWritable {
683
700
  * The corked tail of end(): status, headers, body and the finish events. Split out so a
684
701
  * synchronous answer calls it straight, already inside uWS's own cork.
685
702
  *
686
- * @param {any} data the last body piece
687
- * @param {any} cb
703
+ * @param {string|Buffer|Uint8Array|null|undefined} data the last body piece
704
+ * @param {(() => void)|undefined} cb
688
705
  */
689
706
  _finish(data, cb) {
690
707
  // read before the head is written below, which is what sets the flag: what matters further
691
708
  // down is whether something had already committed the framing, a flushHeaders() or a first
692
709
  // res.write(), not whether this call is about to write the head itself
693
- const headWasAlreadyOut = this.headersSent;
694
- if (!this.headersSent) {
710
+ const headWasAlreadyOut = this.#headOut;
711
+ if (!this.#headOut) {
695
712
  // freshness is not decided here. node's end() knows nothing about conditional requests,
696
713
  // and Express answers 304 from send() and from sendFile(), each of which strips the
697
714
  // entity headers first. Deciding it here made res.end("body") answer 304 and drop the
698
715
  // body the caller had just written.
699
716
  // "unknown" for a code without a message, as node's status line has it. The default 200
700
717
  // with no phrase is not written at all: uWS emits the identical head on its own
701
- if (this.statusCode !== 200 || this.statusText !== undefined) {
702
- this._res.writeStatus(statusLine(this.statusCode, this.statusText));
718
+ if (this.#status !== 200 || this.#statusText !== undefined) {
719
+ this._res.writeStatus(statusLine(this.#status, this.#statusText));
703
720
  }
704
721
  this.writeHeaders(true);
705
722
  }
@@ -714,7 +731,7 @@ module.exports = class Response extends LazyWritable {
714
731
  const closeConnection = this.req._connectionClose === true;
715
732
  // 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
716
733
  // third case, by range
717
- if (this.statusCode === 204 || this.statusCode === 304 || this.statusCode < 200) {
734
+ if (this.#status === 204 || this.#status === 304 || this.#status < 200) {
718
735
  // no body and no length describing one, whatever the caller passed. node decides
719
736
  // this the same way, from the status alone, so res.status(304).end("x") sends the
720
737
  // status and nothing else on either.
@@ -739,7 +756,7 @@ module.exports = class Response extends LazyWritable {
739
756
  if (this.req.method === "HEAD") {
740
757
  const length = Buffer.byteLength(data ?? "");
741
758
  this.headers["content-length"] = String(length);
742
- this._res.endWithoutBody(length.toString(), closeConnection);
759
+ this._res.endWithoutBody(length, closeConnection);
743
760
  } else {
744
761
  // remembered rather than measured: only a caller that asks for content-length pays
745
762
  // for it, and uWS is measuring the same bytes for the wire anyway
@@ -774,7 +791,8 @@ module.exports = class Response extends LazyWritable {
774
791
  */
775
792
  send(body) {
776
793
  if (this.headersSent) {
777
- throw new Error("Can't write body: Response was already sent");
794
+ // what express's send meets first once the head is out is setHeader's refusal
795
+ throw headersSentError("set");
778
796
  }
779
797
  // a typed array is bytes to send, not an object to serialise: res.send(new Uint8Array([104,
780
798
  // 101, 121])) is "hey" and not {"0":104,"1":101,"2":121}. Uint8Array and not every view
@@ -874,10 +892,11 @@ module.exports = class Response extends LazyWritable {
874
892
  * options position is the callback.
875
893
  *
876
894
  * Options: `root`, `maxAge`, `lastModified`, `headers`, `dotfiles` ("allow", "deny" or
877
- * "ignore"), `acceptRanges`, `cacheControl`, `immutable`, `etag` and `setHeaders`.
895
+ * "ignore"), `acceptRanges`, `cacheControl`, `immutable` and `etag`.
878
896
  *
879
897
  * @param {string} path
880
- * @param {import("./options").SendFileOptions} [options]
898
+ * @param {import("./options").SendFileOptions|((err?: Error) => void)} [options] or the callback in
899
+ * its place
881
900
  * @param {(err?: Error) => void} [callback] called once sent, or with the error
882
901
  */
883
902
  sendFile(path, options = new NullObject(), callback) {
@@ -890,7 +909,7 @@ module.exports = class Response extends LazyWritable {
890
909
  throw new TypeError("path must be a string to res.sendFile");
891
910
  }
892
911
  if (typeof options === "function") {
893
- callback = /** @type {any} */ (options);
912
+ callback = options;
894
913
  options = new NullObject();
895
914
  }
896
915
  if (!options) options = new NullObject();
@@ -900,7 +919,7 @@ module.exports = class Response extends LazyWritable {
900
919
  // The router's next and not the route's: express reports a file it could not serve past the
901
920
  // rest of the route, so a four argument handler inside the route never sees it
902
921
  const next = this.req._leaveRoute ?? this.req.next;
903
- const done = /** @type {(err?: any) => void} */ (
922
+ const done = /** @type {(err?: NodeJS.ErrnoException) => void} */ (
904
923
  (err) => {
905
924
  if (callback) return callback(err);
906
925
  if (err && err.code === "EISDIR") return next();
@@ -914,7 +933,9 @@ module.exports = class Response extends LazyWritable {
914
933
  // the branch that is already a number: ms() answers undefined for a duration it cannot
915
934
  // read, and Number.isNaN(undefined) is false, so an unreadable string reached it as NaN
916
935
  const maxAge = Number(
917
- typeof options.maxAge === "string" ? ms(/** @type {any} */ (options.maxAge)) : options.maxAge
936
+ typeof options.maxAge === "string"
937
+ ? ms(/** @type {import("ms").StringValue} */ (options.maxAge))
938
+ : options.maxAge
918
939
  );
919
940
  options.maxAge = Number.isNaN(maxAge) ? 0 : Math.min(Math.max(0, maxAge), MAX_MAXAGE);
920
941
  if (typeof options.lastModified === "undefined") {
@@ -1005,14 +1026,15 @@ module.exports = class Response extends LazyWritable {
1005
1026
  } catch (err) {
1006
1027
  // the fs error itself, carrying its errno and path, with send's status written on
1007
1028
  // it: a missing file is the request's 404, an unreadable one is the server's 500
1008
- return done(asStatError(/** @type {any} */ (err)));
1029
+ return done(asStatError(/** @type {import("./utils.js").HttpError} */ (err)));
1009
1030
  }
1010
1031
  if (stat.isDirectory()) {
1011
1032
  // Express reports a directory as an EISDIR with no status, because send tells it
1012
1033
  // apart from an error: it emits "directory", and res.sendFile has no listener for
1013
1034
  // one. So this is not a 404, and an error handler reading err.code sees the code
1014
1035
  // it expects. Without a callback, done() turns it into a plain next().
1015
- const err = /** @type {any} */ (new Error("EISDIR, read"));
1036
+ /** @type {NodeJS.ErrnoException} */
1037
+ const err = new Error("EISDIR, read");
1016
1038
  err.code = "EISDIR";
1017
1039
  return done(err);
1018
1040
  }
@@ -1040,8 +1062,10 @@ module.exports = class Response extends LazyWritable {
1040
1062
  this.setHeader(header, options.headers[header]);
1041
1063
  }
1042
1064
  }
1043
- if (options.setHeaders) {
1044
- options.setHeaders(/** @type {any} */ (this), fullpath, stat);
1065
+ // express.static's setHeaders, which res.sendFile does not take: express ignores it here,
1066
+ // so it travels under a name only the middleware writes
1067
+ if (options._setHeaders) {
1068
+ options._setHeaders(this, fullpath, stat);
1045
1069
  }
1046
1070
 
1047
1071
  // etag, from the stat and never from the app's "etag fn". send computes this itself with
@@ -1090,7 +1114,10 @@ module.exports = class Response extends LazyWritable {
1090
1114
 
1091
1115
  // range requests
1092
1116
  if (options.acceptRanges) {
1093
- if (this.req.headers.range) {
1117
+ // only the bytes unit, and send checks the header's text for it before parsing:
1118
+ // "items=0-1" or "Bytes=0-1" is not a range request, and answers the whole file
1119
+ const rangeHeader = this.req.headers.range;
1120
+ if (rangeHeader !== undefined && BYTES_RANGE.test(rangeHeader)) {
1094
1121
  // the branch above established the header is there, so range() cannot answer
1095
1122
  // the undefined it uses to mean "no Range header"
1096
1123
  let ranges = /** @type {ReturnType<typeof import("range-parser")>} */ (
@@ -1149,7 +1176,8 @@ module.exports = class Response extends LazyWritable {
1149
1176
  // ECONNABORTED to a callback and never to next(), so aborts stay out of
1150
1177
  // the error middleware.
1151
1178
  if (this.aborted && callback) {
1152
- const err = /** @type {any} */ (new Error("Request aborted"));
1179
+ /** @type {NodeJS.ErrnoException} */
1180
+ const err = new Error("Request aborted");
1153
1181
  err.code = "ECONNABORTED";
1154
1182
  callback(err);
1155
1183
  }
@@ -1267,7 +1295,8 @@ module.exports = class Response extends LazyWritable {
1267
1295
  * what a media type is. res.set does that, and is what Express code should use.
1268
1296
  *
1269
1297
  * @param {string} field
1270
- * @param {any} value an array sends the header once per entry
1298
+ * @param {number|string|readonly string[]|undefined} value an array sends the header once per
1299
+ * entry; undefined is refused, as node refuses it
1271
1300
  * @returns {this}
1272
1301
  * @throws {Error} once the headers have gone out
1273
1302
  * @throws {TypeError} if the name is not a token, the value is undefined, or the value holds a
@@ -1275,7 +1304,7 @@ module.exports = class Response extends LazyWritable {
1275
1304
  */
1276
1305
  setHeader(field, value) {
1277
1306
  if (this.headersSent) {
1278
- throw new Error("Cannot set headers after they are sent to the client");
1307
+ throw headersSentError("set");
1279
1308
  }
1280
1309
  // one Map hit for a name already validated and lowercased: middleware writes the same
1281
1310
  // constant names on every request. Insert-only after validation, so no bad name can enter
@@ -1338,15 +1367,17 @@ module.exports = class Response extends LazyWritable {
1338
1367
  * @returns {void}
1339
1368
  */
1340
1369
  flushHeaders() {
1341
- if (this.headersSent || this.finished || this.aborted) {
1370
+ if (this.#headOut || this.finished || this.aborted) {
1342
1371
  return;
1343
1372
  }
1344
1373
  this._res.cork(() => {
1345
- this.writeHead(this.statusCode);
1374
+ if (!this.headersSent) {
1375
+ this.writeHead(this.statusCode);
1376
+ }
1346
1377
  // the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
1347
1378
  // byte, so writing it again would only cost a crossing
1348
- if (this.statusCode !== 200 || this.statusText !== undefined) {
1349
- this._res.writeStatus(statusLine(this.statusCode, this.statusText));
1379
+ if (this.#status !== 200 || this.#statusText !== undefined) {
1380
+ this._res.writeStatus(statusLine(this.#status, this.#statusText));
1350
1381
  }
1351
1382
  // true, as the chunked path passes for a string chunk: what follows a flush is a body
1352
1383
  // written in pieces, and the framing has to be the one that allows them
@@ -1403,10 +1434,7 @@ module.exports = class Response extends LazyWritable {
1403
1434
  */
1404
1435
  #refuseInformationAfterHead() {
1405
1436
  if (this.headersSent) {
1406
- /** @type {NodeJS.ErrnoException} */
1407
- const err = new Error("Cannot write headers after they are sent to the client");
1408
- err.code = "ERR_HTTP_HEADERS_SENT";
1409
- throw err;
1437
+ throw headersSentError("write");
1410
1438
  }
1411
1439
  }
1412
1440
 
@@ -1439,7 +1467,7 @@ module.exports = class Response extends LazyWritable {
1439
1467
  * node's `assignSocket`, which the http server uses when a response is
1440
1468
  * handed a raw socket. There is no such socket here.
1441
1469
  *
1442
- * @param {any} [socket] node takes one here and there is none to take
1470
+ * @param {import("net").Socket} [socket] node takes one here and there is none to take
1443
1471
  * @returns {void}
1444
1472
  */
1445
1473
  assignSocket(socket) {}
@@ -1447,7 +1475,7 @@ module.exports = class Response extends LazyWritable {
1447
1475
  /**
1448
1476
  * node's `detachSocket`, which the http server uses when a response is
1449
1477
  * handed a raw socket. There is no such socket here.
1450
- * @param {any} [socket] node takes one here and there is none to take
1478
+ * @param {import("net").Socket} [socket] node takes one here and there is none to take
1451
1479
  * @returns {void}
1452
1480
  */
1453
1481
  detachSocket(socket) {}
@@ -1508,10 +1536,10 @@ module.exports = class Response extends LazyWritable {
1508
1536
  const key = name.toLowerCase();
1509
1537
  const current = this.headers[key];
1510
1538
  if (current === undefined) {
1511
- return this.setHeader(name, /** @type {any} */ (value));
1539
+ return this.setHeader(name, value);
1512
1540
  }
1513
- const merged = [].concat(/** @type {any} */ (current), /** @type {any} */ (value));
1514
- return this.setHeader(name, /** @type {any} */ (merged));
1541
+ const merged = /** @type {string[]} */ ([]).concat(current, value);
1542
+ return this.setHeader(name, merged);
1515
1543
  }
1516
1544
 
1517
1545
  /**
@@ -1526,15 +1554,15 @@ module.exports = class Response extends LazyWritable {
1526
1554
  if (typeof Headers === "function" && headers instanceof Headers) {
1527
1555
  for (const name of new Set([...headers.keys()])) {
1528
1556
  if (name === "set-cookie") {
1529
- this.setHeader(name, /** @type {any} */ (headers.getSetCookie()));
1557
+ this.setHeader(name, headers.getSetCookie());
1530
1558
  } else {
1531
- this.setHeader(name, /** @type {any} */ (headers.get(name)));
1559
+ this.setHeader(name, /** @type {string} */ (headers.get(name)));
1532
1560
  }
1533
1561
  }
1534
1562
  return this;
1535
1563
  }
1536
1564
  for (const [name, value] of headers) {
1537
- this.setHeader(name, /** @type {any} */ (value));
1565
+ this.setHeader(name, value);
1538
1566
  }
1539
1567
  return this;
1540
1568
  }
@@ -1551,8 +1579,8 @@ module.exports = class Response extends LazyWritable {
1551
1579
 
1552
1580
  /**
1553
1581
  * The Express name for set(), including the charset it adds to a content-type.
1554
- * @param {any} field a header name, or an object of them
1555
- * @param {any} [value] the header value, or nothing when the first argument is an object
1582
+ * @param {string|object} field a header name, or an object of them
1583
+ * @param {string|string[]} [value] the header value, or nothing when the first argument is an object
1556
1584
  * @returns {this}
1557
1585
  */
1558
1586
  header(field, value) {
@@ -1644,6 +1672,9 @@ module.exports = class Response extends LazyWritable {
1644
1672
  * @param {string} field
1645
1673
  */
1646
1674
  removeHeader(field) {
1675
+ if (this.headersSent) {
1676
+ throw headersSentError("remove");
1677
+ }
1647
1678
  const key = field.toLowerCase();
1648
1679
  // the delete is a runtime call, and helmet removes a header most responses never carry
1649
1680
  if (key in this.headers) {
@@ -1677,12 +1708,13 @@ module.exports = class Response extends LazyWritable {
1677
1708
  * Renders a view and sends it. With a callback the result goes to the callback instead, and
1678
1709
  * nothing is sent. A function in the options position is taken as the callback.
1679
1710
  * @param {string} view view name
1680
- * @param {Record<string, any>} [options] locals for the view
1711
+ * @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for the
1712
+ * view, or the callback in its place
1681
1713
  * @param {(err: Error|null, html?: string) => void} [callback]
1682
1714
  */
1683
1715
  render(view, options, callback) {
1684
1716
  if (typeof options === "function") {
1685
- callback = /** @type {any} */ (options);
1717
+ callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
1686
1718
  options = {};
1687
1719
  }
1688
1720
  if (!options) {
@@ -1720,7 +1752,7 @@ module.exports = class Response extends LazyWritable {
1720
1752
  const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
1721
1753
  // cookie-parser hangs the secret on the request, so it is read off it rather than
1722
1754
  // declared here: without that middleware there is none, which is what this checks
1723
- const req = /** @type {any} */ (this.req);
1755
+ const req = /** @type {{secret?: string}} */ (this.req);
1724
1756
  if (opt.signed && !req.secret) {
1725
1757
  // the message has to read like this: it is the one Express throws, and it names the
1726
1758
  // thing that is actually missing rather than the library that noticed
@@ -1739,7 +1771,7 @@ module.exports = class Response extends LazyWritable {
1739
1771
  delete opt.maxAge;
1740
1772
  }
1741
1773
  if (opt.signed) {
1742
- val = "s:" + sign(val, req.secret);
1774
+ val = "s:" + sign(val, /** @type {string} */ (req.secret));
1743
1775
  }
1744
1776
 
1745
1777
  if (opt.path == null) {
@@ -1784,7 +1816,7 @@ module.exports = class Response extends LazyWritable {
1784
1816
  * Answers according to the Accept header, calling the handler whose key matches best. A
1785
1817
  * `default` key catches everything else; without one an unmatched request gets 406.
1786
1818
  * Sets Vary: Accept.
1787
- * @param {Record<string, any>} object handlers keyed by extension or mime type
1819
+ * @param {Record<string, Function>} object handlers keyed by extension or mime type
1788
1820
  * @returns {this}
1789
1821
  */
1790
1822
  format(object) {
@@ -1822,11 +1854,14 @@ module.exports = class Response extends LazyWritable {
1822
1854
  * @returns {this}
1823
1855
  */
1824
1856
  json(body) {
1857
+ const hot = this.app._hot();
1858
+ // serialised before the type is set, as express orders it: a body JSON.stringify refuses,
1859
+ // a BigInt for one, throws out of here with the response's headers as they were
1860
+ const json = stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape);
1825
1861
  if (!this.headers["content-type"]) {
1826
1862
  this.headers["content-type"] = JSON_UTF8;
1827
1863
  }
1828
- const hot = this.app._hot();
1829
- return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
1864
+ return this.send(json);
1830
1865
  }
1831
1866
 
1832
1867
  /**
@@ -1878,7 +1913,7 @@ module.exports = class Response extends LazyWritable {
1878
1913
 
1879
1914
  /**
1880
1915
  * Adds to the Link header, one entry per key, the key being the rel.
1881
- * @param {Record<string, any>} links rel to url
1916
+ * @param {Record<string, string>} links rel to url
1882
1917
  * @returns {this}
1883
1918
  */
1884
1919
  links(links) {
@@ -2001,7 +2036,7 @@ module.exports = class Response extends LazyWritable {
2001
2036
  vary(field) {
2002
2037
  // the vary package decides: it throws when there is no field at all, and does nothing at
2003
2038
  // all for an empty list, which is not the same thing and used to be refused here as well
2004
- vary(/** @type {any} */ (this), field);
2039
+ vary(/** @type {import("http").ServerResponse} */ (/** @type {unknown} */ (this)), field);
2005
2040
  return this;
2006
2041
  }
2007
2042
 
@@ -2036,4 +2071,5 @@ module.exports = class Response extends LazyWritable {
2036
2071
 
2037
2072
  // res.contentType is res.type under express's other name. On the prototype rather than an instance
2038
2073
  // field, which wrote one own property per response in the constructor.
2039
- /** @type {any} */ (module.exports.prototype).contentType = module.exports.prototype.type;
2074
+ /** @type {{contentType?: typeof module.exports.prototype.type}} */ (module.exports.prototype).contentType =
2075
+ module.exports.prototype.type;
package/src/route.js CHANGED
@@ -70,7 +70,7 @@ class Route {
70
70
  *
71
71
  * @param {any} req a Request, or whatever a caller that built this Route by hand is serving
72
72
  * @param {any} res the matching response
73
- * @param {(err?: any) => void} done called when the route is finished with the request, with
73
+ * @param {(err?: unknown) => void} done called when the route is finished with the request, with
74
74
  * whatever error it ended on
75
75
  */
76
76
  dispatch(req, res, done) {
@@ -113,7 +113,7 @@ class Route {
113
113
  return done(err);
114
114
  }
115
115
 
116
- const handle = /** @type {any} */ (layer).handle;
116
+ const handle = /** @type {{handle: Function}} */ (layer).handle;
117
117
  // an error only reaches the four-argument handlers, and everything else only runs
118
118
  // while there is no error, which is the same rule ordinary dispatch follows
119
119
  if (err) {
@@ -146,7 +146,7 @@ class Route {
146
146
  * Refuses a handler that could never be called, worded as express words it.
147
147
  *
148
148
  * @param {string} method the verb this was registered for, for the message
149
- * @param {any[]} handlers
149
+ * @param {unknown[]} handlers
150
150
  */
151
151
  function checkRouteHandlers(method, handlers) {
152
152
  for (const handle of handlers) {