fulmine.js 5.19.2 → 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,22 +38,29 @@ const {
38
38
  withUtf8Charset,
39
39
  asStatError,
40
40
  httpError,
41
+ headersSentError,
42
+ applyWriteHead,
41
43
  contentTypeFor,
42
44
  statTag,
43
45
  cachedStat,
44
46
  NullObject
45
47
  } = require("./utils.js");
46
- const { Writable } = require("stream");
47
48
  const { isAbsolute } = require("path");
48
49
  const fs = require("fs");
49
50
  const Path = require("path");
50
51
  const statuses = require("statuses");
51
52
  const { sign } = require("cookie-signature");
52
- // events is faster at init, tseep is faster at sending events
53
- // since we create a ton of objects and dont send a ton of events, its better to use events here
54
- const { EventEmitter } = require("events");
55
- const http = require("http");
56
53
  const ms = require("ms");
54
+ const Socket = require("./socket.js");
55
+ const { LazyWritable } = require("./lazy-writable.js");
56
+ const {
57
+ kOutHeaders,
58
+ kShapeMode,
59
+ VALIDATED_HEADER_NAMES,
60
+ HEADER_NAME_BUF,
61
+ HEADER_VALUE_BUF,
62
+ statusLine
63
+ } = require("./response-utils.js");
57
64
 
58
65
  // How much a chunked response may gather before it is handed to uWS. Measured on uWS alone with
59
66
  // 33 KB written in 500 pieces: 16.2ms handed over one piece at a time, 0.79ms in 4 KB blocks,
@@ -67,48 +74,6 @@ const COALESCE_LIMIT = 16 * 1024;
67
74
  // here up goes straight through.
68
75
  const COALESCE_BELOW = 4 * 1024;
69
76
 
70
- const outgoingMessage = new http.OutgoingMessage();
71
- const symbols = Object.getOwnPropertySymbols(outgoingMessage);
72
- // if a future node renames it, fall back to a private symbol rather than writing a property
73
- // literally named "undefined", which is what indexing with undefined would do
74
- const kOutHeaders = symbols.find((s) => s.toString() === "Symbol(kOutHeaders)") ?? Symbol("kOutHeaders");
75
- // node's emitters tombstone a removed listener's slot instead of deleting it when this flag is
76
- // set, which is what keeps _events in a stable shape. EventEmitter.init sets it, and never runs
77
- // for the lazily-materialized response, so it is set by hand; a future rename degrades to the
78
- // delete, not to an error
79
- const kShapeMode =
80
- Object.getOwnPropertySymbols(new EventEmitter()).find((s) => s.toString() === "Symbol(shapeMode)") ??
81
- Symbol("shapeMode");
82
- // names setHeader has validated and lowercased, so the constant names middleware writes per
83
- // request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
84
- // the never-throwing readers keep their plain toLowerCase
85
- const VALIDATED_HEADER_NAMES = new Map();
86
- // The names and values that recur on every response, kept as Buffers for the uWS crossing: a
87
- // Buffer is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is
88
- // not here just misses the lookup and crosses as the string it was. Names must stay lowercase,
89
- // which is how writeHeaders receives them.
90
- const HEADER_NAME_BUF = { __proto__: null };
91
- const HEADER_VALUE_BUF = { __proto__: null };
92
- for (const s of ["connection", "keep-alive", "content-type", "vary", "x-powered-by", "content-encoding"]) {
93
- HEADER_NAME_BUF[s] = Buffer.from(s);
94
- }
95
- for (const s of [
96
- "keep-alive",
97
- "timeout=10",
98
- "close",
99
- "Fulmine",
100
- "Accept-Encoding",
101
- "text/html; charset=utf-8",
102
- "text/plain; charset=utf-8",
103
- "application/json; charset=utf-8",
104
- "application/octet-stream",
105
- "gzip",
106
- "br",
107
- "deflate",
108
- "zstd"
109
- ]) {
110
- HEADER_VALUE_BUF[s] = Buffer.from(s);
111
- }
112
77
  const HIGH_WATERMARK = 128 * 1024;
113
78
  // the exact string json() writes, so send() can skip recomputing the charset on it
114
79
  const JSON_UTF8 = "application/json; charset=utf-8";
@@ -116,283 +81,8 @@ const JSON_UTF8 = "application/json; charset=utf-8";
116
81
  // than written out, since a year is already longer than any cache will honour.
117
82
  const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
118
83
 
119
- class Socket extends EventEmitter {
120
- /**
121
- * The Socket's own error listener, shared across sockets: an error on the stand-in closes it,
122
- * which is the close the connection trackers wait for. EventEmitter calls it with this = the
123
- * emitter.
124
- *
125
- * @this {any}
126
- * @param {any} err
127
- */
128
- static _onError(err) {
129
- this.emit("close");
130
- }
131
-
132
- /**
133
- * Enough of a node socket for the middleware that reaches for one. There is no socket object
134
- * in uWS to hand over, so this stands in and forwards what it can to the response.
135
- *
136
- * @param {any} response
137
- */
138
- constructor(response) {
139
- super();
140
- this.response = response;
141
- this[kShapeMode] = true;
142
- // middleware assigns to this one, which is why it is a field rather than a getter: express
143
- // reads socket.encrypted for req.protocol and a proxy shim writes it
144
- this.encrypted = response.req.app.ssl;
145
- this.localPort = response.req.app.port;
146
- // on-finished reads socket.readable before anything else, and a socket without one reads
147
- // as a request that is already over
148
- this.readable = true;
149
-
150
- // shared, not an arrow: one per process instead of one per materialized socket
151
- this.on("error", Socket._onError);
152
- }
153
-
154
- /** Whether anything more can be written, which stops being true once the response is done. */
155
- get writable() {
156
- return !this.response.finished;
157
- }
158
-
159
- /** The peer, as node reports it. Reading it out of µWS is slow, so the request caches it. */
160
- get remoteAddress() {
161
- return this.response.req.parsedIp;
162
- }
163
-
164
- /** A native µWS call almost no caller makes, so it stays behind its getter. */
165
- get remotePort() {
166
- return this.response.req._res.getRemotePort();
167
- }
168
-
169
- /**
170
- * node's socket carries these three and applications call them on a request they mean to hold
171
- * open, almost always to take the timeout off. µWS has no per socket timeout reachable from
172
- * javascript, so they do nothing and hand the socket back the way node's do. n8n's chat trigger
173
- * calls setTimeout on every webhook, and without it the workflow answered 500.
174
- * @returns {this}
175
- */
176
- setTimeout() {
177
- return this;
178
- }
179
-
180
- /** @returns {this} */
181
- setKeepAlive() {
182
- return this;
183
- }
184
-
185
- /** @returns {this} */
186
- setNoDelay() {
187
- return this;
188
- }
189
-
190
- /**
191
- * Finishes the response through the socket, which is how the middleware that only knows
192
- * about sockets ends one.
193
- * @param {any} [body]
194
- */
195
- end(body) {
196
- this.response.end(body);
197
- }
198
-
199
- /**
200
- * What a server side socket answers about itself. µWS owns the connection, so these follow the
201
- * response: it is open until the response is over, and it was never a socket being dialled.
202
- */
203
- get destroyed() {
204
- return this.response.finished === true;
205
- }
206
-
207
- /** @returns {string} "open" until the response is over, as a served socket reads. */
208
- get readyState() {
209
- return this.response.finished === true ? "closed" : "open";
210
- }
211
-
212
- /** @returns {boolean} never: this end was accepted, not dialled. */
213
- get connecting() {
214
- return false;
215
- }
216
-
217
- /** @returns {boolean} never, for the same reason. */
218
- get pending() {
219
- return false;
220
- }
221
-
222
- /**
223
- * The end of the connection node reports here. There is no address to read back from µWS, so
224
- * this is the port the application bound and the family the peer arrived on.
225
- * @returns {{address: string|undefined, family: string, port: number|undefined}}
226
- */
227
- address() {
228
- const remote = this.response.req.parsedIp;
229
- return {
230
- address: this.response.req.app._listenHost,
231
- family: remote?.includes(":") ? "IPv6" : "IPv4",
232
- port: this.localPort
233
- };
234
- }
235
-
236
- /**
237
- * Drops the connection, which is what an application does to a client it will not serve. node
238
- * takes an error and re-emits it; this closes and says so through 'close', since there is no
239
- * socket underneath to carry an error of its own.
240
- * @returns {this}
241
- */
242
- destroy() {
243
- this.close();
244
- return this;
245
- }
246
-
247
- /** @returns {this} */
248
- destroySoon() {
249
- this.close();
250
- return this;
251
- }
252
-
253
- /**
254
- * Holds and resumes the body arriving on this connection, which is the only half of node's
255
- * pause() that means anything here: the response is written when the application writes it.
256
- * @returns {this}
257
- */
258
- pause() {
259
- this.response.req.pause();
260
- return this;
261
- }
262
-
263
- /** @returns {this} */
264
- resume() {
265
- this.response.req.resume();
266
- return this;
267
- }
268
-
269
- /**
270
- * node writes these bytes past the response, straight onto the connection. There is no way
271
- * past µWS's framing here, so they go through the response instead, which is what the
272
- * middleware writing to a socket means by it.
273
- *
274
- * @param {any} chunk
275
- * @param {any} [encoding]
276
- * @param {any} [callback]
277
- * @returns {boolean}
278
- */
279
- write(chunk, encoding, callback) {
280
- return this.response.write(chunk, encoding, callback);
281
- }
282
-
283
- /** The event loop is µWS's, so there is nothing to hold open or let go. @returns {this} */
284
- ref() {
285
- return this;
286
- }
287
-
288
- /** @returns {this} */
289
- unref() {
290
- return this;
291
- }
292
-
293
- /** Closes the connection outright, without finishing a response first. */
294
- close() {
295
- if (this.response.finished) {
296
- return;
297
- }
298
- this.response.finished = true;
299
- this.emit("close");
300
- this.response._res.close();
301
- }
302
- }
303
-
304
- // One status line per code, built on first use: the default path, with no custom reason phrase,
305
- // paid a template string and a trim per request for a line that never changes. Bounded to real
306
- // HTTP codes so a wild writeHead value cannot grow the array or flip it into dictionary mode.
307
- const STATUS_LINES = [];
308
- /**
309
- * @param {number} code
310
- * @param {string|undefined} text an explicit reason phrase, which bypasses the cache
311
- * @returns {string} the uWS status line, e.g. "200 OK"
312
- */
313
- function statusLine(code, text) {
314
- if (text === undefined && Number.isInteger(code) && code >= 100 && code <= 999) {
315
- return STATUS_LINES[code] ?? (STATUS_LINES[code] = code + " " + (statuses.message[code] ?? "unknown"));
316
- }
317
- return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
318
- }
319
-
320
- /**
321
- * A Writable that has not built its state yet, the mirror of LazyReadable in request.js and there
322
- * for the same reason: a response is a Writable because middleware expects one, and the ordinary
323
- * one never uses it. `send()` reaches `end()`, which is overridden here and goes straight to
324
- * _finish, so the WritableState is allocated for every response and read by nobody. It is needed
325
- * only by res.write(), by a stream piped into the response, by cork and by the writableX getters.
326
- *
327
- * `Response extends LazyWritable`, whose prototype is Writable's, so `res instanceof Writable`
328
- * stays true and every Writable method is reachable; what is missing is `_writableState`, built on
329
- * the first touch. Measured at 45 nanoseconds a response on the machine this was written on.
330
- *
331
- * As on the Readable side the wrapping is generated rather than written out: every own member of
332
- * Writable's prototype gets a version that materialises first, so there is no list to keep in step.
333
- * Missing one would not be a slow path, it would be a TypeError on `undefined._writableState`.
334
- */
335
- class LazyWritableBase {}
336
- Object.setPrototypeOf(LazyWritableBase.prototype, Writable.prototype);
337
- Object.setPrototypeOf(LazyWritableBase, Writable);
338
-
339
- // what the chain says at runtime, said again for the type checker, which cannot see a prototype
340
- // being reassigned
341
- const LazyWritable = /** @type {typeof Writable} */ (/** @type {unknown} */ (LazyWritableBase));
342
-
343
- /**
344
- * Builds the stream this object has been pretending to be. Idempotent: everything reachable from
345
- * outside goes through it, so it is called far more often than it does anything.
346
- *
347
- * EventEmitter's init keeps an _events that is already there, so both the shape the constructor
348
- * wrote and any listener added before this survive it.
349
- *
350
- * @param {any} stream
351
- */
352
- function materialiseWritable(stream) {
353
- if (stream._writableState === undefined) {
354
- Writable.call(stream);
355
- }
356
- }
357
-
358
- for (const member of [
359
- ...Object.getOwnPropertyNames(Writable.prototype),
360
- ...Object.getOwnPropertySymbols(Writable.prototype)
361
- ]) {
362
- if (member === "constructor") {
363
- continue;
364
- }
365
- const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Writable.prototype, member));
366
- if (typeof descriptor.value === "function") {
367
- const inner = descriptor.value;
368
- Object.defineProperty(LazyWritableBase.prototype, member, {
369
- ...descriptor,
370
- /** @this {any} @param {...any} args */
371
- value: function (...args) {
372
- materialiseWritable(this);
373
- return inner.apply(this, args);
374
- }
375
- });
376
- } else if (descriptor.get || descriptor.set) {
377
- const innerGet = descriptor.get;
378
- const innerSet = descriptor.set;
379
- Object.defineProperty(LazyWritableBase.prototype, member, {
380
- ...descriptor,
381
- get: innerGet
382
- ? /** @this {any} */ function () {
383
- materialiseWritable(this);
384
- return innerGet.call(this);
385
- }
386
- : undefined,
387
- set: innerSet
388
- ? /** @this {any} @param {any} value */ function (value) {
389
- materialiseWritable(this);
390
- innerSet.call(this, value);
391
- }
392
- : undefined
393
- });
394
- }
395
- }
84
+ // what send takes as a range request: the bytes unit, checked on the header's text before parsing
85
+ const BYTES_RANGE = /^ *bytes=/;
396
86
 
397
87
  module.exports = class Response extends LazyWritable {
398
88
  /** @type {Socket|null} */
@@ -409,10 +99,8 @@ module.exports = class Response extends LazyWritable {
409
99
  *
410
100
  * uWS charges for a write against everything already buffered behind it, so a chunked response
411
101
  * written in many small pieces costs quadratically once it passes the socket's own buffer:
412
- * measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them.
413
- * Handing it the same bytes in blocks costs 0.4ms. Nothing here changes what goes on the wire,
414
- * only how many calls it takes to put it there.
415
- * Null until the first chunked write, since a res.send never queues anything.
102
+ * measured on uWS alone, 500 writes of 66 bytes take 13ms against 0.06ms for 100 of them, and
103
+ * the same bytes in blocks cost 0.4ms. Null until the first chunked write.
416
104
  * @type {Buffer[]|null}
417
105
  */
418
106
  #queued = null;
@@ -423,7 +111,20 @@ module.exports = class Response extends LazyWritable {
423
111
  /** Whether a flush is already booked for the end of this turn. */
424
112
  #flushBooked = false;
425
113
 
426
- /** @type {any} */
114
+ /** Whether the status line and the headers have reached uWS, which only a body write does. */
115
+ #headOut = false;
116
+
117
+ /**
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}
121
+ */
122
+ #status = 200;
123
+
124
+ /** @type {string|undefined} */
125
+ #statusText = undefined;
126
+
127
+ /** @type {Response["headers"]|null} */
427
128
  #outHeaders = null;
428
129
 
429
130
  /**
@@ -437,9 +138,9 @@ module.exports = class Response extends LazyWritable {
437
138
  * two that describe the connection, since every response carries them, and x-powered-by only
438
139
  * when the setting asks for it.
439
140
  *
440
- * @param {any} res the uWS response
441
- * @param {any} req the Request, already built, which is where the connection header is read from
442
- * @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
443
144
  */
444
145
  constructor(res, req, app) {
445
146
  super();
@@ -448,6 +149,7 @@ module.exports = class Response extends LazyWritable {
448
149
  // the order node's own Writable constructor lays down, so the hidden class is the one every
449
150
  // other stream in the process has, and node's init keeps this object when the state is
450
151
  // finally built
152
+ /** @type {Record<string, Function|undefined>} */
451
153
  this._events = {
452
154
  close: undefined,
453
155
  error: undefined,
@@ -500,26 +202,22 @@ module.exports = class Response extends LazyWritable {
500
202
  this.body = undefined;
501
203
  // what was handed to uWS, kept so a caller asking for content-length after the fact can be
502
204
  // answered, see get(). Undefined until the response ends, and for one that sends no body
503
- /** @type {string|Buffer|undefined} */
205
+ /** @type {string|Buffer|Uint8Array|undefined} */
504
206
  this._sentBody = undefined;
505
207
  // false while the uWS route handler is still in its synchronous window, where uWS holds
506
208
  // the socket corked itself; the two uWS entry points flip it once that window closes
507
209
  this._corkNeeded = false;
508
- // shared methods, not arrows: two closures and a once() wrapper here were four
509
- // allocations per request. EventEmitter calls listeners with this = the emitter.
210
+ // shared methods, not arrows: two closures and a once() wrapper here were four allocations
211
+ // per request. EventEmitter calls listeners with this = the emitter.
510
212
  //
511
213
  // Written into the map rather than through on(). A stream arrives with its _events already
512
214
  // shaped, "close" and "error" among the keys and every value undefined, so filling two of
513
- // them is the same hidden class on() would have produced and none of its work: the argument
514
- // check, the newListener emission, the is-there-one-already branch and the max listener
515
- // count. Two calls per response, and the profile put them at 6% of a hello-world.
215
+ // them is the same hidden class on() would produce and none of its work. Two calls per
216
+ // response, and the profile put them at 6% of a hello-world.
516
217
  //
517
- // The condition is the whole safety of it: a fresh response has no listeners, so both slots
518
- // are free, and anything else falls back to on(). Adding one later goes through on() as
519
- // usual and finds what this wrote, because this wrote what on() writes.
520
- // cast because _events and _eventsCount are EventEmitter's own bookkeeping and carry no
521
- // type: they are what on() writes, and this writes the same two entries
522
- const self = /** @type {any} */ (this);
218
+ // The condition is the safety of it: a fresh response has no listeners, so both slots are
219
+ // free, and anything else falls back to on(), which finds what this wrote.
220
+ const self = this;
523
221
  const events = self._events;
524
222
  if (
525
223
  self._eventsCount === 0 &&
@@ -556,17 +254,15 @@ module.exports = class Response extends LazyWritable {
556
254
  /**
557
255
  * Drops the connection, which is how an application abandons a response it cannot finish: a
558
256
  * download whose source dies mid-transfer has to leave the client with a reset rather than a
559
- * truncated body it would take for the whole file. node destroys the socket here and µWS's
560
- * close() is the same thing; without it the client waited for bytes that were never coming and
561
- * the request hung until its own timeout. LibreChat's download route is written exactly that
562
- * way, `stream.on("error", () => res.destroy())`.
257
+ * truncated body. node destroys the socket here and uWS's close() is the same thing; without it
258
+ * the client waited for bytes that never came. LibreChat's download route is written that way,
259
+ * `stream.on("error", () => res.destroy())`.
563
260
  *
564
- * A response that is over, or one whose client is already gone, only tears the stream down:
565
- * there is nothing left to close, and touching an aborted µWS response is a use after free.
566
- * One difference from node stays: writableEnded reads true after this, because it is answered
567
- * from the same finished flag the close sets, where node leaves it false until end() is called.
261
+ * A response that is over, or one whose client is gone, only tears the stream down: touching an
262
+ * aborted uWS response is a use after free. writableEnded reads true after this, where node
263
+ * leaves it false until end() is called.
568
264
  *
569
- * @param {any} [error]
265
+ * @param {Error} [error] whatever the caller is destroying the response with
570
266
  * @returns {this}
571
267
  */
572
268
  destroy(error) {
@@ -596,7 +292,7 @@ module.exports = class Response extends LazyWritable {
596
292
  return;
597
293
  }
598
294
  this._pendingLinked = false;
599
- const pending = /** @type {any} */ (this)._pendingIn;
295
+ const pending = /** @type {{_pendingIn?: {head: Response|null}}} */ (this)._pendingIn;
600
296
  const prev = this._pendingPrev;
601
297
  const next = this._pendingNext;
602
298
  if (prev) {
@@ -613,12 +309,9 @@ module.exports = class Response extends LazyWritable {
613
309
 
614
310
  /**
615
311
  * Where node keeps the outgoing headers of an OutgoingMessage. Only code going through node's
616
- * own header path ever looks, cookie-session being the one in this project's tests, so the
617
- * proxy standing in for it is built on the first look rather than on every response: it was a
618
- * proxy, a handler object and two closures each time, for something almost nothing reads.
619
- *
620
- * A setter as well, because node assigns to this slot when it resets the headers, and a getter
621
- * on its own would make that throw.
312
+ * own header path looks, cookie-session being the one in this project's tests, so the proxy
313
+ * standing in for it is built on the first look: it was a proxy, a handler object and two
314
+ * closures per response otherwise. A setter too, because node assigns to this slot on a reset.
622
315
  */
623
316
  get [kOutHeaders]() {
624
317
  if (!this.#outHeaders) {
@@ -654,7 +347,7 @@ module.exports = class Response extends LazyWritable {
654
347
  * over, as node's does; the request's `socket` is the same object and stays, so it comes
655
348
  * through here instead.
656
349
  *
657
- * @returns {any}
350
+ * @returns {Socket}
658
351
  */
659
352
  _socketShim() {
660
353
  if (!this.#socket) {
@@ -712,7 +405,8 @@ module.exports = class Response extends LazyWritable {
712
405
  * The booked flush. Static, so a response that never writes in pieces allocates nothing for it:
713
406
  * nextTick forwards the receiver as an argument.
714
407
  *
715
- * @param {any} res
408
+ * @param {any} res the response whose queue is being flushed. Loose because naming the class
409
+ * inside its own body makes the checker see two unrelated `this` types
716
410
  */
717
411
  static #flushOnTick(res) {
718
412
  res.#flushBooked = false;
@@ -726,7 +420,7 @@ module.exports = class Response extends LazyWritable {
726
420
  * said how much there would be. Backpressure comes back as onWritable, which is what defers the
727
421
  * callback rather than dropping the chunk.
728
422
  *
729
- * @param {any} chunk
423
+ * @param {any} chunk whatever a Writable was handed, which node does not narrow
730
424
  * @param {BufferEncoding} encoding
731
425
  * @param {(err?: Error|null) => void} callback
732
426
  */
@@ -744,13 +438,15 @@ module.exports = class Response extends LazyWritable {
744
438
 
745
439
  this.writingChunk = true;
746
440
  this._res.cork(() => {
747
- if (!this.headersSent) {
748
- this.writeHead(this.statusCode);
441
+ if (!this.#headOut) {
442
+ if (!this.headersSent) {
443
+ this.writeHead(this.statusCode);
444
+ }
749
445
  // "unknown" and not the bare number: node writes that reason phrase for a code it
750
446
  // has no message for, so the raw status lines match. The default 200 with no
751
447
  // phrase is uWS's own head, byte for byte, so it is not written at all
752
- if (this.statusCode !== 200 || this.statusText !== undefined) {
753
- 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));
754
450
  }
755
451
  this.writeHeaders(typeof chunk === "string");
756
452
  }
@@ -819,61 +515,43 @@ module.exports = class Response extends LazyWritable {
819
515
 
820
516
  /**
821
517
  * Sets the status and, optionally, a batch of headers, the way node does. The second argument
822
- * is either the status message or the headers, since node allows both shapes.
823
- *
824
- * Nothing is 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.
825
521
  *
826
522
  * Every header goes through setHeader and not through set. This is node's method, not
827
- * Express's: Express does not override it, so a content-type given here keeps the value it was
828
- * given, where `res.set("content-type", "text/html")` would have a charset appended to it. A
829
- * handler that builds its own response and writes it with writeHead is how every meta-framework
830
- * on top of Express answers, @astrojs/node and @sveltejs/adapter-node included, so the charset
831
- * was being added to pages nobody asked it for.
523
+ * Express's: a content-type given here keeps the value it was given, where res.set would append
524
+ * a charset. Every meta-framework on Express answers with writeHead, @astrojs/node and
525
+ * @sveltejs/adapter-node included, so the charset was added to pages nobody asked it for.
832
526
  *
833
527
  * @param {number} statusCode
834
- * @param {string|Record<string, any>|any[]} [statusMessage] the reason phrase, or the headers
835
- * @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]
836
531
  * @returns {this}
837
532
  */
838
533
  writeHead(statusCode, statusMessage, headers) {
839
- this.statusCode = statusCode;
840
- if (typeof statusMessage === "string") {
841
- this.statusText = statusMessage;
842
- }
843
- if (!headers) {
844
- if (!statusMessage) return this;
845
- // the two-argument shape, where what looked like a reason phrase is the headers. A
846
- // string reaching here was already taken as the phrase above and simply has no keys.
847
- headers = /** @type {Record<string, any>} */ (statusMessage);
848
- }
849
- if (Array.isArray(headers)) {
850
- // node takes a flat list here, name then value, and not a list of pairs. An odd length
851
- // is the caller's mistake and node names the argument in what it throws
852
- if (headers.length % 2 !== 0) {
853
- /** @type {NodeJS.ErrnoException} */
854
- const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
855
- err.code = "ERR_INVALID_ARG_VALUE";
856
- throw err;
857
- }
858
- for (let i = 0; i < headers.length; i += 2) {
859
- this.setHeader(headers[i], headers[i + 1]);
860
- }
861
- return this;
534
+ if (this.headersSent) {
535
+ throw headersSentError("write");
862
536
  }
863
- for (const header in headers) {
864
- this.setHeader(header, headers[header]);
537
+ this.statusCode = statusCode;
538
+ const reason = applyWriteHead(this, statusMessage, headers);
539
+ if (reason !== undefined) {
540
+ this.statusText = reason;
865
541
  }
542
+ this.#status = statusCode;
543
+ this.#statusText = this.statusText;
544
+ this.headersSent = true;
866
545
  return this;
867
546
  }
868
547
 
869
548
  /**
870
- * Writes every header set so far to uWS, which is the point of no return. Content-Length is
871
- * not one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out
872
- * here and kept on totalSize, where it also turns chunked framing off.
549
+ * Writes every header set so far to uWS, which is the point of no return. Content-Length is not
550
+ * one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out here
551
+ * and kept on totalSize, where it also turns chunked framing off.
873
552
  *
874
- * One writeHeader per header on purpose. Packing the whole head into a single writeStatus
875
- * works on the wire, and was measured slower: constant header strings cross the boundary
876
- * already flat, while the packed head is concatenated fresh per response, see issue #11.
553
+ * One writeHeader per header on purpose. Packing the whole head into a single writeStatus works
554
+ * on the wire and measured slower: constant header strings cross already flat, see issue #11.
877
555
  *
878
556
  * @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
879
557
  * differ on what they know about the body
@@ -890,12 +568,10 @@ module.exports = class Response extends LazyWritable {
890
568
  // the value here is nearly always the 10-char "keep-alive", which paid a scan per response
891
569
  const closing =
892
570
  typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
893
- // for..in over an object some responses delete from, which is the shape the request side
894
- // was taken off for #rawHeadersEntries. It stays here, and the difference is where the
895
- // deletes are: a 200 with a body performs none. Only 204, 304, 205, the freshness branch of
896
- // sendFile and removeHeader do, this object is built fresh per response, so a dictionary one
897
- // of them made costs that response and nothing after it. The request side deleted on every
898
- // request, which is what made it worth a different structure
571
+ // for..in over an object some responses delete from, the shape the request side was taken
572
+ // off for #rawHeadersEntries. It stays here because the deletes are rare: only 204, 304,
573
+ // 205, the freshness branch of sendFile and removeHeader do it, and this object is built
574
+ // fresh per response. The request side deleted on every request
899
575
  for (const header in headers) {
900
576
  if (closing && header === "keep-alive") {
901
577
  continue;
@@ -919,16 +595,19 @@ module.exports = class Response extends LazyWritable {
919
595
  }
920
596
  }
921
597
  this.headersSent = true;
598
+ this.#headOut = true;
922
599
  }
923
600
 
924
601
  /**
925
- * What node calls before writing a body when the caller never called writeHead. Here there is
926
- * 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.
927
606
  */
928
607
  _implicitHeader() {
929
- // compatibility function
930
- // usually should send headers but this is useless for us
931
- this.writeHead(this.statusCode);
608
+ if (!this.headersSent) {
609
+ this.writeHead(this.statusCode);
610
+ }
932
611
  }
933
612
 
934
613
  /**
@@ -967,17 +646,29 @@ module.exports = class Response extends LazyWritable {
967
646
  }
968
647
 
969
648
  /**
970
- * @param {any} [data]
971
- * @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]
972
654
  * @returns {this}
973
655
  */
974
- end(data, cb) {
656
+ end(data, encoding, cb) {
975
657
  if (typeof data === "function") {
976
658
  cb = data;
977
659
  data = undefined;
660
+ encoding = undefined;
661
+ } else if (typeof encoding === "function") {
662
+ cb = encoding;
663
+ encoding = undefined;
978
664
  }
979
665
  if (typeof cb !== "function") {
980
- 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);
981
672
  }
982
673
 
983
674
  if (this.writingChunk) {
@@ -989,7 +680,11 @@ module.exports = class Response extends LazyWritable {
989
680
  if (this.finished) {
990
681
  return this;
991
682
  }
992
- 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
+ }
993
688
  // uWS holds the socket corked for the synchronous window of its route handler, and
994
689
  // cork inside cork is a passthrough: the wrapper and its closure are only paid once the
995
690
  // answer has outlived that window, which is what _corkNeeded records
@@ -1005,42 +700,38 @@ module.exports = class Response extends LazyWritable {
1005
700
  * The corked tail of end(): status, headers, body and the finish events. Split out so a
1006
701
  * synchronous answer calls it straight, already inside uWS's own cork.
1007
702
  *
1008
- * @param {any} data
1009
- * @param {any} cb
703
+ * @param {string|Buffer|Uint8Array|null|undefined} data the last body piece
704
+ * @param {(() => void)|undefined} cb
1010
705
  */
1011
706
  _finish(data, cb) {
1012
707
  // read before the head is written below, which is what sets the flag: what matters further
1013
708
  // down is whether something had already committed the framing, a flushHeaders() or a first
1014
709
  // res.write(), not whether this call is about to write the head itself
1015
- const headWasAlreadyOut = this.headersSent;
1016
- if (!this.headersSent) {
1017
- // freshness is not decided here. node's end() knows nothing about conditional
1018
- // requests, and Express answers 304 from send() and from sendFile(), each of
1019
- // which strips the entity headers first. Deciding it here meant res.end("body")
1020
- // answered 304 and dropped the body that the caller had just written.
1021
- // "unknown" for a code without a message, as node's status line has it.
1022
- // The default 200 with no phrase is not written at all: uWS emits the identical
1023
- // "HTTP/1.1 200 OK" head on its own, and the crossing costs more than it says
1024
- if (this.statusCode !== 200 || this.statusText !== undefined) {
1025
- this._res.writeStatus(statusLine(this.statusCode, this.statusText));
710
+ const headWasAlreadyOut = this.#headOut;
711
+ if (!this.#headOut) {
712
+ // freshness is not decided here. node's end() knows nothing about conditional requests,
713
+ // and Express answers 304 from send() and from sendFile(), each of which strips the
714
+ // entity headers first. Deciding it here made res.end("body") answer 304 and drop the
715
+ // body the caller had just written.
716
+ // "unknown" for a code without a message, as node's status line has it. The default 200
717
+ // with no phrase is not written at all: uWS emits the identical head on its own
718
+ if (this.#status !== 200 || this.#statusText !== undefined) {
719
+ this._res.writeStatus(statusLine(this.#status, this.#statusText));
1026
720
  }
1027
721
  this.writeHeaders(true);
1028
722
  }
1029
723
  const contentLength = this.headers["content-length"];
1030
724
  // The client said this connection ends here, and it is this end() that has to make it so.
1031
- // µWS closes by itself for a bare "close" and not for a list, so "keep-alive, close" left
1032
- // the socket open and the bytes after that request were read as another one: node closes
1033
- // there, and a server that does not is a server the client and it disagree with about how
1034
- // many requests were sent. See saysClose.
725
+ // uWS closes by itself for a bare "close" and not for a list, so "keep-alive, close" left
726
+ // the socket open and the bytes after that request were read as another one. See saysClose.
1035
727
  //
1036
- // Only where a length goes out with it. endWithoutBody takes the flag as its second
728
+ // Only where a length goes out with it: endWithoutBody takes the flag as its second
1037
729
  // argument and reads the first as the length whatever it holds, so asking it to close
1038
- // without one writes "Content-Length: 9223372036854775808" onto a 204. Those two paths keep
1039
- // µWS's own rule, which closes for a bare "close" and not for a list.
730
+ // without one writes "Content-Length: 9223372036854775808" onto a 204.
1040
731
  const closeConnection = this.req._connectionClose === true;
1041
732
  // 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
1042
733
  // third case, by range
1043
- if (this.statusCode === 204 || this.statusCode === 304 || this.statusCode < 200) {
734
+ if (this.#status === 204 || this.#status === 304 || this.#status < 200) {
1044
735
  // no body and no length describing one, whatever the caller passed. node decides
1045
736
  // this the same way, from the status alone, so res.status(304).end("x") sends the
1046
737
  // status and nothing else on either.
@@ -1051,11 +742,9 @@ module.exports = class Response extends LazyWritable {
1051
742
  // whatever is still queued goes first: end() must not overtake the body written before it
1052
743
  this.#flushQueued(null);
1053
744
  // The head has already gone out without a length, which is what flushHeaders() and the
1054
- // first res.write() both do, so this response is committed to chunked framing and a
1055
- // length can no longer describe it. node is committed the same way: after a flush,
1056
- // res.end("body") sends a chunk, not a Content-Length. Handing the body to uWS's end()
1057
- // here would have it append a length to a head that already said otherwise, which is
1058
- // what the comparison test caught.
745
+ // first res.write() both do, so this response is committed to chunked framing. node is
746
+ // committed the same way: after a flush, res.end("body") sends a chunk. Handing the
747
+ // body to uWS's end() here would append a length to a head that already said otherwise
1059
748
  if (data) {
1060
749
  this._res.write(data);
1061
750
  this._sentBody = data;
@@ -1067,7 +756,7 @@ module.exports = class Response extends LazyWritable {
1067
756
  if (this.req.method === "HEAD") {
1068
757
  const length = Buffer.byteLength(data ?? "");
1069
758
  this.headers["content-length"] = String(length);
1070
- this._res.endWithoutBody(length.toString(), closeConnection);
759
+ this._res.endWithoutBody(length, closeConnection);
1071
760
  } else {
1072
761
  // remembered rather than measured: only a caller that asks for content-length pays
1073
762
  // for it, and uWS is measuring the same bytes for the wire anyway
@@ -1102,7 +791,8 @@ module.exports = class Response extends LazyWritable {
1102
791
  */
1103
792
  send(body) {
1104
793
  if (this.headersSent) {
1105
- 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");
1106
796
  }
1107
797
  // a typed array is bytes to send, not an object to serialise: res.send(new Uint8Array([104,
1108
798
  // 101, 121])) is "hey" and not {"0":104,"1":101,"2":121}. Uint8Array and not every view
@@ -1157,13 +847,10 @@ module.exports = class Response extends LazyWritable {
1157
847
  }
1158
848
  // the ETag belongs here rather than in end(): node's end() does not produce one, so
1159
849
  // res.end() and res.redirect() must not either. It has to be set before end() reads
1160
- // req.fresh, which compares If-None-Match against it.
1161
- // body is defined by the time it gets here, so an empty one still earns an ETag. Testing
1162
- // its truthiness instead meant send("") and send(null) came back without one.
1163
- // Every method by default, not only GET and HEAD: gating it looked safe, freshness being
1164
- // defined over those two alone, and express's own suite failed on it, "should send ETag
1165
- // in response to <METHOD> request" exists per method. The "etag methods" setting is that
1166
- // gate as an opt-in, see issue #10.
850
+ // req.fresh, which compares If-None-Match against it. body is defined by the time it gets
851
+ // here, so an empty one still earns an ETag; testing truthiness left send("") and
852
+ // send(null) without one. Every method by default, not only GET and HEAD: express's own
853
+ // suite has "should send ETag in response to <METHOD> request" per method, see issue #10
1167
854
  const hot = this.app._hot();
1168
855
  const etagFn = hot.etagFn;
1169
856
  if (
@@ -1205,10 +892,11 @@ module.exports = class Response extends LazyWritable {
1205
892
  * options position is the callback.
1206
893
  *
1207
894
  * Options: `root`, `maxAge`, `lastModified`, `headers`, `dotfiles` ("allow", "deny" or
1208
- * "ignore"), `acceptRanges`, `cacheControl`, `immutable`, `etag` and `setHeaders`.
895
+ * "ignore"), `acceptRanges`, `cacheControl`, `immutable` and `etag`.
1209
896
  *
1210
897
  * @param {string} path
1211
- * @param {import("./options").SendFileOptions} [options]
898
+ * @param {import("./options").SendFileOptions|((err?: Error) => void)} [options] or the callback in
899
+ * its place
1212
900
  * @param {(err?: Error) => void} [callback] called once sent, or with the error
1213
901
  */
1214
902
  sendFile(path, options = new NullObject(), callback) {
@@ -1221,20 +909,17 @@ module.exports = class Response extends LazyWritable {
1221
909
  throw new TypeError("path must be a string to res.sendFile");
1222
910
  }
1223
911
  if (typeof options === "function") {
1224
- callback = /** @type {any} */ (options);
912
+ callback = options;
1225
913
  options = new NullObject();
1226
914
  }
1227
915
  if (!options) options = new NullObject();
1228
- // the callback is optional: without one, errors go to next(). The router assigns req.next
1229
- // before any handler can run, so by the time sendFile is reachable it is always there.
1230
- // Express's completion handler, exactly: a callback hears everything and the response is
1231
- // left alone, so it can still answer 200 after a 404 error. Without one, a directory
1232
- // falls through as a plain next(), and aborts and write errors go nowhere.
1233
- // the router's next and not the route's: express reports a file it could not serve past
1234
- // the rest of the route, so a four argument handler written inside the route never sees
1235
- // it. _leaveRoute is that; req.next is kept for everything else, see Walk#stepOutOfRoute
916
+ // the callback is optional: without one, errors go to next(). Express's completion handler
917
+ // exactly: a callback hears everything and the response is left alone, so it can still
918
+ // answer 200 after a 404 error. Without one, a directory falls through as a plain next().
919
+ // The router's next and not the route's: express reports a file it could not serve past the
920
+ // rest of the route, so a four argument handler inside the route never sees it
1236
921
  const next = this.req._leaveRoute ?? this.req.next;
1237
- const done = /** @type {(err?: any) => void} */ (
922
+ const done = /** @type {(err?: NodeJS.ErrnoException) => void} */ (
1238
923
  (err) => {
1239
924
  if (callback) return callback(err);
1240
925
  if (err && err.code === "EISDIR") return next();
@@ -1242,16 +927,15 @@ module.exports = class Response extends LazyWritable {
1242
927
  }
1243
928
  );
1244
929
  // default options
1245
- // Normalised the way send does, and it is not fussiness: max-age takes a non-negative
1246
- // integer count of seconds, so 0.5, -1 and Infinity are all invalid, and a client that
1247
- // cannot read the directive may throw away the whole Cache-Control header with it. A
1248
- // fractional maxAge came out as "max-age=0.5" here, a negative one as "max-age=-1" and
1249
- // Infinity as "max-age=Infinity".
1250
- // Number() around the lot, and not only around the branch that is already a number: ms()
1251
- // answers undefined for a duration it cannot read, and Number.isNaN(undefined) is false,
1252
- // so an unreadable string reached the header as "max-age=NaN".
930
+ // Normalised the way send does: max-age takes a non-negative integer count of seconds, so
931
+ // 0.5, -1 and Infinity are all invalid, and a client that cannot read the directive may
932
+ // throw away the whole Cache-Control header. Number() around the lot and not only around
933
+ // the branch that is already a number: ms() answers undefined for a duration it cannot
934
+ // read, and Number.isNaN(undefined) is false, so an unreadable string reached it as NaN
1253
935
  const maxAge = Number(
1254
- 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
1255
939
  );
1256
940
  options.maxAge = Number.isNaN(maxAge) ? 0 : Math.min(Math.max(0, maxAge), MAX_MAXAGE);
1257
941
  if (typeof options.lastModified === "undefined") {
@@ -1342,14 +1026,15 @@ module.exports = class Response extends LazyWritable {
1342
1026
  } catch (err) {
1343
1027
  // the fs error itself, carrying its errno and path, with send's status written on
1344
1028
  // it: a missing file is the request's 404, an unreadable one is the server's 500
1345
- return done(asStatError(/** @type {any} */ (err)));
1029
+ return done(asStatError(/** @type {import("./utils.js").HttpError} */ (err)));
1346
1030
  }
1347
1031
  if (stat.isDirectory()) {
1348
1032
  // Express reports a directory as an EISDIR with no status, because send tells it
1349
1033
  // apart from an error: it emits "directory", and res.sendFile has no listener for
1350
1034
  // one. So this is not a 404, and an error handler reading err.code sees the code
1351
1035
  // it expects. Without a callback, done() turns it into a plain next().
1352
- const err = /** @type {any} */ (new Error("EISDIR, read"));
1036
+ /** @type {NodeJS.ErrnoException} */
1037
+ const err = new Error("EISDIR, read");
1353
1038
  err.code = "EISDIR";
1354
1039
  return done(err);
1355
1040
  }
@@ -1377,8 +1062,10 @@ module.exports = class Response extends LazyWritable {
1377
1062
  this.setHeader(header, options.headers[header]);
1378
1063
  }
1379
1064
  }
1380
- if (options.setHeaders) {
1381
- 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);
1382
1069
  }
1383
1070
 
1384
1071
  // etag, from the stat and never from the app's "etag fn". send computes this itself with
@@ -1427,7 +1114,10 @@ module.exports = class Response extends LazyWritable {
1427
1114
 
1428
1115
  // range requests
1429
1116
  if (options.acceptRanges) {
1430
- 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)) {
1431
1121
  // the branch above established the header is there, so range() cannot answer
1432
1122
  // the undefined it uses to mean "no Range header"
1433
1123
  let ranges = /** @type {ReturnType<typeof import("range-parser")>} */ (
@@ -1486,7 +1176,8 @@ module.exports = class Response extends LazyWritable {
1486
1176
  // ECONNABORTED to a callback and never to next(), so aborts stay out of
1487
1177
  // the error middleware.
1488
1178
  if (this.aborted && callback) {
1489
- const err = /** @type {any} */ (new Error("Request aborted"));
1179
+ /** @type {NodeJS.ErrnoException} */
1180
+ const err = new Error("Request aborted");
1490
1181
  err.code = "ECONNABORTED";
1491
1182
  callback(err);
1492
1183
  }
@@ -1604,7 +1295,8 @@ module.exports = class Response extends LazyWritable {
1604
1295
  * what a media type is. res.set does that, and is what Express code should use.
1605
1296
  *
1606
1297
  * @param {string} field
1607
- * @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
1608
1300
  * @returns {this}
1609
1301
  * @throws {Error} once the headers have gone out
1610
1302
  * @throws {TypeError} if the name is not a token, the value is undefined, or the value holds a
@@ -1612,7 +1304,7 @@ module.exports = class Response extends LazyWritable {
1612
1304
  */
1613
1305
  setHeader(field, value) {
1614
1306
  if (this.headersSent) {
1615
- throw new Error("Cannot set headers after they are sent to the client");
1307
+ throw headersSentError("set");
1616
1308
  }
1617
1309
  // one Map hit for a name already validated and lowercased: middleware writes the same
1618
1310
  // constant names on every request. Insert-only after validation, so no bad name can enter
@@ -1642,14 +1334,13 @@ module.exports = class Response extends LazyWritable {
1642
1334
  }
1643
1335
 
1644
1336
  /**
1645
- * Throws away any header that could not be written, so that flushing this response cannot fail
1646
- * on one. setHeader refuses these on the way in, but `res.headers` is the live object, so an
1337
+ * Throws away any header that could not be written, so flushing this response cannot fail on
1338
+ * one. setHeader refuses these on the way in, but `res.headers` is the live object, so an
1647
1339
  * assignment into that still gets a value in here.
1648
1340
  *
1649
1341
  * Only the error page calls it. A throw out of the flush there is not recoverable: the error
1650
- * page is what runs after a throw, so it would be the second one, with nobody left to catch
1651
- * it, and on the node shim that is the process. Everything writable is left alone, since a
1652
- * middleware's own headers belong on the error response too.
1342
+ * page is what runs after a throw, so nobody is left to catch it. Everything writable is left
1343
+ * alone, since a middleware's own headers belong on the error response too.
1653
1344
  *
1654
1345
  * @returns {void}
1655
1346
  */
@@ -1663,30 +1354,30 @@ module.exports = class Response extends LazyWritable {
1663
1354
 
1664
1355
  /**
1665
1356
  * Hands the status line and the headers over now, without waiting for a body, which is node's
1666
- * flushHeaders(). Callers use it to let the client start on the head while the body is still
1667
- * being produced, and one of them is `@angular/ssr`'s writeResponseToNodeResponse, which calls
1668
- * it before streaming a rendered page.
1357
+ * flushHeaders(). `@angular/ssr`'s writeResponseToNodeResponse calls it before streaming a page.
1669
1358
  *
1670
1359
  * **The head does not reach the wire here.** uWS holds it until the first body chunk, so a
1671
1360
  * client sees nothing until then, where express answers at once. `beginWrite` is uWS's API for
1672
1361
  * this and is unusable: it emits a stray CRLF before the first chunk size, which node's parser
1673
- * rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0, the latest release.
1362
+ * rejects as HPE_INVALID_CHUNK_SIZE. Checked against v20.69.0.
1674
1363
  *
1675
1364
  * A second call does nothing, as node's does. Nothing is written for a response already
1676
- * finished or aborted: uWS has let go of it by then.
1365
+ * finished or aborted.
1677
1366
  *
1678
1367
  * @returns {void}
1679
1368
  */
1680
1369
  flushHeaders() {
1681
- if (this.headersSent || this.finished || this.aborted) {
1370
+ if (this.#headOut || this.finished || this.aborted) {
1682
1371
  return;
1683
1372
  }
1684
1373
  this._res.cork(() => {
1685
- this.writeHead(this.statusCode);
1374
+ if (!this.headersSent) {
1375
+ this.writeHead(this.statusCode);
1376
+ }
1686
1377
  // the same rule the chunked write path follows: uWS emits the 200 head itself, byte for
1687
1378
  // byte, so writing it again would only cost a crossing
1688
- if (this.statusCode !== 200 || this.statusText !== undefined) {
1689
- 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));
1690
1381
  }
1691
1382
  // true, as the chunked path passes for a string chunk: what follows a flush is a body
1692
1383
  // written in pieces, and the framing has to be the one that allows them
@@ -1695,13 +1386,11 @@ module.exports = class Response extends LazyWritable {
1695
1386
  }
1696
1387
 
1697
1388
  /**
1698
- * node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want, so a
1699
- * browser can start fetching them while the server is still rendering.
1389
+ * node's `writeEarlyHints`, which sends a `103` carrying the resources the page will want.
1700
1390
  *
1701
- * **Nothing is sent here.** µWebSockets.js has no API for an informational response, so the
1702
- * hints cannot reach the wire, and this exists so that code written for Express keeps running
1703
- * rather than dying on "res.writeEarlyHints is not a function". The callback is still called,
1704
- * because node calls it once the hints are out and a caller may be waiting on it.
1391
+ * **Nothing is sent here.** uWebSockets.js has no API for an informational response, so this
1392
+ * exists only so code written for Express keeps running instead of dying on
1393
+ * "res.writeEarlyHints is not a function". The callback is still called, as node calls it.
1705
1394
  *
1706
1395
  * `writeContinue` and `writeProcessing` below are the same story with `100` and `102`.
1707
1396
  *
@@ -1745,10 +1434,7 @@ module.exports = class Response extends LazyWritable {
1745
1434
  */
1746
1435
  #refuseInformationAfterHead() {
1747
1436
  if (this.headersSent) {
1748
- /** @type {NodeJS.ErrnoException} */
1749
- const err = new Error("Cannot write headers after they are sent to the client");
1750
- err.code = "ERR_HTTP_HEADERS_SENT";
1751
- throw err;
1437
+ throw headersSentError("write");
1752
1438
  }
1753
1439
  }
1754
1440
 
@@ -1781,7 +1467,7 @@ module.exports = class Response extends LazyWritable {
1781
1467
  * node's `assignSocket`, which the http server uses when a response is
1782
1468
  * handed a raw socket. There is no such socket here.
1783
1469
  *
1784
- * @param {any} [socket]
1470
+ * @param {import("net").Socket} [socket] node takes one here and there is none to take
1785
1471
  * @returns {void}
1786
1472
  */
1787
1473
  assignSocket(socket) {}
@@ -1789,7 +1475,7 @@ module.exports = class Response extends LazyWritable {
1789
1475
  /**
1790
1476
  * node's `detachSocket`, which the http server uses when a response is
1791
1477
  * handed a raw socket. There is no such socket here.
1792
- * @param {any} [socket]
1478
+ * @param {import("net").Socket} [socket] node takes one here and there is none to take
1793
1479
  * @returns {void}
1794
1480
  */
1795
1481
  detachSocket(socket) {}
@@ -1850,10 +1536,10 @@ module.exports = class Response extends LazyWritable {
1850
1536
  const key = name.toLowerCase();
1851
1537
  const current = this.headers[key];
1852
1538
  if (current === undefined) {
1853
- return this.setHeader(name, /** @type {any} */ (value));
1539
+ return this.setHeader(name, value);
1854
1540
  }
1855
- const merged = [].concat(/** @type {any} */ (current), /** @type {any} */ (value));
1856
- return this.setHeader(name, /** @type {any} */ (merged));
1541
+ const merged = /** @type {string[]} */ ([]).concat(current, value);
1542
+ return this.setHeader(name, merged);
1857
1543
  }
1858
1544
 
1859
1545
  /**
@@ -1868,15 +1554,15 @@ module.exports = class Response extends LazyWritable {
1868
1554
  if (typeof Headers === "function" && headers instanceof Headers) {
1869
1555
  for (const name of new Set([...headers.keys()])) {
1870
1556
  if (name === "set-cookie") {
1871
- this.setHeader(name, /** @type {any} */ (headers.getSetCookie()));
1557
+ this.setHeader(name, headers.getSetCookie());
1872
1558
  } else {
1873
- this.setHeader(name, /** @type {any} */ (headers.get(name)));
1559
+ this.setHeader(name, /** @type {string} */ (headers.get(name)));
1874
1560
  }
1875
1561
  }
1876
1562
  return this;
1877
1563
  }
1878
1564
  for (const [name, value] of headers) {
1879
- this.setHeader(name, /** @type {any} */ (value));
1565
+ this.setHeader(name, value);
1880
1566
  }
1881
1567
  return this;
1882
1568
  }
@@ -1893,8 +1579,8 @@ module.exports = class Response extends LazyWritable {
1893
1579
 
1894
1580
  /**
1895
1581
  * The Express name for set(), including the charset it adds to a content-type.
1896
- * @param {any} field a header name, or an object of them
1897
- * @param {any} [value]
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
1898
1584
  * @returns {this}
1899
1585
  */
1900
1586
  header(field, value) {
@@ -1986,6 +1672,9 @@ module.exports = class Response extends LazyWritable {
1986
1672
  * @param {string} field
1987
1673
  */
1988
1674
  removeHeader(field) {
1675
+ if (this.headersSent) {
1676
+ throw headersSentError("remove");
1677
+ }
1989
1678
  const key = field.toLowerCase();
1990
1679
  // the delete is a runtime call, and helmet removes a header most responses never carry
1991
1680
  if (key in this.headers) {
@@ -2019,12 +1708,13 @@ module.exports = class Response extends LazyWritable {
2019
1708
  * Renders a view and sends it. With a callback the result goes to the callback instead, and
2020
1709
  * nothing is sent. A function in the options position is taken as the callback.
2021
1710
  * @param {string} view view name
2022
- * @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
2023
1713
  * @param {(err: Error|null, html?: string) => void} [callback]
2024
1714
  */
2025
1715
  render(view, options, callback) {
2026
1716
  if (typeof options === "function") {
2027
- callback = /** @type {any} */ (options);
1717
+ callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
2028
1718
  options = {};
2029
1719
  }
2030
1720
  if (!options) {
@@ -2062,7 +1752,7 @@ module.exports = class Response extends LazyWritable {
2062
1752
  const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
2063
1753
  // cookie-parser hangs the secret on the request, so it is read off it rather than
2064
1754
  // declared here: without that middleware there is none, which is what this checks
2065
- const req = /** @type {any} */ (this.req);
1755
+ const req = /** @type {{secret?: string}} */ (this.req);
2066
1756
  if (opt.signed && !req.secret) {
2067
1757
  // the message has to read like this: it is the one Express throws, and it names the
2068
1758
  // thing that is actually missing rather than the library that noticed
@@ -2081,7 +1771,7 @@ module.exports = class Response extends LazyWritable {
2081
1771
  delete opt.maxAge;
2082
1772
  }
2083
1773
  if (opt.signed) {
2084
- val = "s:" + sign(val, req.secret);
1774
+ val = "s:" + sign(val, /** @type {string} */ (req.secret));
2085
1775
  }
2086
1776
 
2087
1777
  if (opt.path == null) {
@@ -2126,7 +1816,7 @@ module.exports = class Response extends LazyWritable {
2126
1816
  * Answers according to the Accept header, calling the handler whose key matches best. A
2127
1817
  * `default` key catches everything else; without one an unmatched request gets 406.
2128
1818
  * Sets Vary: Accept.
2129
- * @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
2130
1820
  * @returns {this}
2131
1821
  */
2132
1822
  format(object) {
@@ -2164,11 +1854,14 @@ module.exports = class Response extends LazyWritable {
2164
1854
  * @returns {this}
2165
1855
  */
2166
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);
2167
1861
  if (!this.headers["content-type"]) {
2168
1862
  this.headers["content-type"] = JSON_UTF8;
2169
1863
  }
2170
- const hot = this.app._hot();
2171
- return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
1864
+ return this.send(json);
2172
1865
  }
2173
1866
 
2174
1867
  /**
@@ -2220,7 +1913,7 @@ module.exports = class Response extends LazyWritable {
2220
1913
 
2221
1914
  /**
2222
1915
  * Adds to the Link header, one entry per key, the key being the rel.
2223
- * @param {Record<string, any>} links rel to url
1916
+ * @param {Record<string, string>} links rel to url
2224
1917
  * @returns {this}
2225
1918
  */
2226
1919
  links(links) {
@@ -2343,7 +2036,7 @@ module.exports = class Response extends LazyWritable {
2343
2036
  vary(field) {
2344
2037
  // the vary package decides: it throws when there is no field at all, and does nothing at
2345
2038
  // all for an empty list, which is not the same thing and used to be refused here as well
2346
- vary(/** @type {any} */ (this), field);
2039
+ vary(/** @type {import("http").ServerResponse} */ (/** @type {unknown} */ (this)), field);
2347
2040
  return this;
2348
2041
  }
2349
2042
 
@@ -2365,10 +2058,9 @@ module.exports = class Response extends LazyWritable {
2365
2058
 
2366
2059
  /**
2367
2060
  * Whether end() has been called. node sets this one there and writableFinished later, once the
2368
- * bytes are out; here end() hands the whole response to µWS, so the two are the same moment.
2061
+ * bytes are out; here end() hands the whole response to uWS, so the two are the same moment.
2369
2062
  * Without it the base property answered false forever, and an application that asks whether it
2370
- * has already answered - LibreChat's agent stream does, before it decides whether to keep a
2371
- * subscription - carried on writing to a response that was over.
2063
+ * has already answered, as LibreChat's agent stream does, kept writing to a dead response.
2372
2064
  */
2373
2065
  // @ts-expect-error TS2611, the accessor replacing the base property is deliberate. Expect
2374
2066
  // rather than ignore, so it fails loudly if it ever stops applying.
@@ -2379,4 +2071,5 @@ module.exports = class Response extends LazyWritable {
2379
2071
 
2380
2072
  // res.contentType is res.type under express's other name. On the prototype rather than an instance
2381
2073
  // field, which wrote one own property per response in the constructor.
2382
- /** @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;