fulmine.js 5.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1360 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+
18
+ const cookie = require("cookie");
19
+ const mime = require("mime-types");
20
+ const vary = require("vary");
21
+ const encodeUrl = require("encodeurl");
22
+ const contentDisposition = require("content-disposition");
23
+ const {
24
+ normalizeType,
25
+ stringify,
26
+ UP_PATH_REGEXP,
27
+ decode,
28
+ containsDotFile,
29
+ isPreconditionFailure,
30
+ isRangeFresh,
31
+ escapeHtml,
32
+ withDefaultCharset,
33
+ withUtf8Charset,
34
+ asStatError,
35
+ httpError,
36
+ contentTypeFor,
37
+ statTag,
38
+ NullObject
39
+ } = require("./utils.js");
40
+ const { Writable } = require("stream");
41
+ const { isAbsolute } = require("path");
42
+ const fs = require("fs");
43
+ const Path = require("path");
44
+ const statuses = require("statuses");
45
+ const { sign } = require("cookie-signature");
46
+ // events is faster at init, tseep is faster at sending events
47
+ // since we create a ton of objects and dont send a ton of events, its better to use events here
48
+ const { EventEmitter } = require("events");
49
+ const http = require("http");
50
+ const ms = require("ms");
51
+
52
+ const outgoingMessage = new http.OutgoingMessage();
53
+ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
54
+ // if a future node renames it, fall back to a private symbol rather than writing a property
55
+ // literally named "undefined", which is what indexing with undefined would do
56
+ const kOutHeaders = symbols.find((s) => s.toString() === "Symbol(kOutHeaders)") ?? Symbol("kOutHeaders");
57
+ const HIGH_WATERMARK = 128 * 1024;
58
+ // Statuses whose message carries no body, so no Content-Length may describe one either. 1xx is
59
+ // the third case and is checked by range rather than listed.
60
+ const STATUSES_WITHOUT_BODY = new Set([204, 304]);
61
+ // send's ceiling for maxAge, one year in milliseconds. Anything larger is clamped to it rather
62
+ // than written out, since a year is already longer than any cache will honour.
63
+ const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
64
+
65
+ class Socket extends EventEmitter {
66
+ /**
67
+ * Enough of a node socket for the middleware that reaches for one. There is no socket object
68
+ * in uWS to hand over, so this stands in and forwards what it can to the response.
69
+ *
70
+ * @param {any} response
71
+ */
72
+ constructor(response) {
73
+ super();
74
+ this.response = response;
75
+
76
+ this.on("error", (err) => {
77
+ this.emit("close");
78
+ });
79
+ }
80
+
81
+ /** Whether anything more can be written, which stops being true once the response is done. */
82
+ get writable() {
83
+ return !this.response.finished;
84
+ }
85
+
86
+ /**
87
+ * Finishes the response through the socket, which is how the middleware that only knows
88
+ * about sockets ends one.
89
+ * @param {any} [body]
90
+ */
91
+ end(body) {
92
+ this.response.end(body);
93
+ }
94
+
95
+ /** Closes the connection outright, without finishing a response first. */
96
+ close() {
97
+ if (this.response.finished) {
98
+ return;
99
+ }
100
+ this.response.finished = true;
101
+ this.emit("close");
102
+ this.response._res.close();
103
+ }
104
+ }
105
+
106
+ module.exports = class Response extends Writable {
107
+ /** @type {Socket|null} */
108
+ #socket = null;
109
+
110
+ #ended = false;
111
+
112
+ /** @type {((err?: Error|null) => void)|null} */
113
+ #pendingCallback = null;
114
+
115
+ /** @type {any} */
116
+ #outHeaders = null;
117
+
118
+ req;
119
+
120
+ /**
121
+ * Built for every request, right after the Request it belongs to. The headers start with the
122
+ * two that describe the connection, since every response carries them, and x-powered-by only
123
+ * when the setting asks for it.
124
+ *
125
+ * @param {any} res the uWS response
126
+ * @param {any} req the Request, already built, which is where the connection header is read from
127
+ * @param {any} app the application or router this request arrived at
128
+ */
129
+ constructor(res, req, app) {
130
+ super();
131
+ this._req = req;
132
+ this._res = res;
133
+ this.headersSent = false;
134
+ this.app = app;
135
+ this.locals = new NullObject();
136
+ this.finished = false;
137
+ this.aborted = false;
138
+ this.statusCode = 200;
139
+ this.statusText = undefined;
140
+ this.chunkedTransfer = true;
141
+ this.totalSize = 0;
142
+ this.writingChunk = false;
143
+ this.headers = {
144
+ connection: "keep-alive",
145
+ "keep-alive": "timeout=10"
146
+ };
147
+ // the client asked for the connection to be closed, and uWS closes it, so saying otherwise
148
+ // would be telling the client something the transport contradicts. A declarative response
149
+ // cannot do this, being written once and not per request.
150
+ if (req._connectionClose) {
151
+ this.headers.connection = "close";
152
+ }
153
+ if (this.app.get("x-powered-by")) {
154
+ this.headers["x-powered-by"] = "Fulmine";
155
+ }
156
+
157
+ this.body = undefined;
158
+ this.on("error", (err) => {
159
+ if (this.finished) {
160
+ return;
161
+ }
162
+ this._res.cork(() => {
163
+ this._res.close();
164
+ this.finished = true;
165
+ this.#socket?.emit("close");
166
+ });
167
+ });
168
+ this.once("close", () => {
169
+ this.#ended = true;
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Where node keeps the outgoing headers of an OutgoingMessage. Only code going through node's
175
+ * own header path ever looks, cookie-session being the one in this project's tests, so the
176
+ * proxy standing in for it is built on the first look rather than on every response: it was a
177
+ * proxy, a handler object and two closures each time, for something almost nothing reads.
178
+ *
179
+ * A setter as well, because node assigns to this slot when it resets the headers, and a getter
180
+ * on its own would make that throw.
181
+ */
182
+ get [kOutHeaders]() {
183
+ if (!this.#outHeaders) {
184
+ this.#outHeaders = new Proxy(this.headers, {
185
+ set: (obj, prop, value) => {
186
+ this.set(prop, value[1]);
187
+ return true;
188
+ },
189
+ get: (obj, prop) => {
190
+ return obj[prop];
191
+ }
192
+ });
193
+ }
194
+ return this.#outHeaders;
195
+ }
196
+
197
+ set [kOutHeaders](value) {
198
+ this.#outHeaders = value;
199
+ }
200
+
201
+ /**
202
+ * A socket-shaped object for middleware that reaches for one, built on first ask and kept
203
+ * from then on. null once the response is over, as node reports it.
204
+ * @returns {Socket|null}
205
+ */
206
+ get socket() {
207
+ if (this.#ended) return null;
208
+ if (!this.#socket) {
209
+ this.#socket = new Socket(this);
210
+ }
211
+ return this.#socket;
212
+ }
213
+
214
+ /**
215
+ * Writable's sink. Sends the headers if they have not gone yet, then hands the chunk to uWS,
216
+ * either as a chunk of a chunked response or through tryEnd when a Content-Length said how
217
+ * much there would be. Backpressure comes back as onWritable, which is what defers the
218
+ * callback rather than dropping the chunk.
219
+ *
220
+ * @param {any} chunk
221
+ * @param {BufferEncoding} encoding
222
+ * @param {(err?: Error|null) => void} callback
223
+ */
224
+ _write(chunk, encoding, callback) {
225
+ if (this.aborted) {
226
+ /** @type {NodeJS.ErrnoException} */
227
+ const err = new Error("Request aborted");
228
+ err.code = "ECONNABORTED";
229
+ return this.destroy(err);
230
+ }
231
+ if (this.finished) {
232
+ const err = new Error("Response already finished");
233
+ return this.destroy(err);
234
+ }
235
+
236
+ this.writingChunk = true;
237
+ this._res.cork(() => {
238
+ if (!this.headersSent) {
239
+ this.writeHead(this.statusCode);
240
+ const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? "";
241
+ this._res.writeStatus(`${this.statusCode} ${statusMessage}`.trim());
242
+ this.writeHeaders(typeof chunk === "string");
243
+ }
244
+
245
+ if (!Buffer.isBuffer(chunk) && !(chunk instanceof ArrayBuffer)) {
246
+ chunk = Buffer.from(chunk);
247
+ chunk = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength);
248
+ }
249
+
250
+ if (this.chunkedTransfer) {
251
+ const ok = this._res.write(chunk);
252
+ if (ok) {
253
+ this.writingChunk = false;
254
+ callback(null);
255
+ } else {
256
+ this.#pendingCallback = callback;
257
+ this._res.onWritable(() => {
258
+ if (this.aborted || this.finished) return true;
259
+ const cb = this.#pendingCallback;
260
+ this.#pendingCallback = null;
261
+ this.writingChunk = false;
262
+ if (cb) cb(null);
263
+ return true;
264
+ });
265
+ }
266
+ } else {
267
+ const lastOffset = this._res.getWriteOffset();
268
+ const [ok, done] = this._res.tryEnd(chunk, this.totalSize);
269
+ if (done) {
270
+ super.end();
271
+ this.finished = true;
272
+ this.writingChunk = false;
273
+ this.#socket?.emit("close");
274
+ callback(null);
275
+ } else if (!ok) {
276
+ this._res.ab = chunk;
277
+ this._res.abOffset = lastOffset;
278
+ let handlerUsed = false;
279
+ this._res.onWritable((offset) => {
280
+ if (this.finished || handlerUsed) return true;
281
+ const [ok, done] = this._res.tryEnd(
282
+ this._res.ab.slice(offset - this._res.abOffset),
283
+ this.totalSize
284
+ );
285
+ if (done) {
286
+ this.finished = true;
287
+ this.#socket?.emit("close");
288
+ }
289
+ if (ok) {
290
+ this.writingChunk = false;
291
+ handlerUsed = true;
292
+ callback(null);
293
+ }
294
+ return ok;
295
+ });
296
+ } else {
297
+ this.writingChunk = false;
298
+ callback(null);
299
+ }
300
+ }
301
+ });
302
+ }
303
+
304
+ /**
305
+ * Sets the status and, optionally, a batch of headers, the way node does. The second argument
306
+ * is either the status message or the headers, since node allows both shapes.
307
+ *
308
+ * Nothing is written here despite the name: the headers go out when the body does.
309
+ *
310
+ * @param {number} statusCode
311
+ * @param {string|Record<string, any>} [statusMessage] the reason phrase, or the headers
312
+ * @param {Record<string, any>} [headers]
313
+ * @returns {this}
314
+ */
315
+ writeHead(statusCode, statusMessage, headers) {
316
+ this.statusCode = statusCode;
317
+ if (typeof statusMessage === "string") {
318
+ this.statusText = statusMessage;
319
+ }
320
+ if (!headers) {
321
+ if (!statusMessage) return this;
322
+ // the two-argument shape, where what looked like a reason phrase is the headers. A
323
+ // string reaching here was already taken as the phrase above and simply has no keys.
324
+ headers = /** @type {Record<string, any>} */ (statusMessage);
325
+ }
326
+ for (const header in headers) {
327
+ this.set(header, headers[header]);
328
+ }
329
+ return this;
330
+ }
331
+
332
+ /**
333
+ * Writes every header set so far to uWS, which is the point of no return. Content-Length is
334
+ * not one of them: uWS wants the length through tryEnd or endWithoutBody, so it is taken out
335
+ * here and kept on totalSize, where it also turns chunked framing off.
336
+ *
337
+ * @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
338
+ * differ on what they know about the body
339
+ */
340
+ writeHeaders(utf8) {
341
+ // Keep-Alive describes a connection that is being kept alive, so node leaves it out once
342
+ // the connection is closing. That happens both when the client asked and when something
343
+ // else set the header on the way out, which is what a proxy passing an upstream response
344
+ // through does.
345
+ const connection = this.headers["connection"];
346
+ const closing = typeof connection === "string" && connection.toLowerCase() === "close";
347
+ for (const header in this.headers) {
348
+ if (closing && header === "keep-alive") {
349
+ continue;
350
+ }
351
+ const value = this.headers[header];
352
+ if (header === "content-length") {
353
+ // if content-length is set, disable chunked transfer encoding, since size is known
354
+ this.chunkedTransfer = false;
355
+ this.totalSize = parseInt(value);
356
+ continue;
357
+ }
358
+ if (Array.isArray(value)) {
359
+ for (const val of value) {
360
+ this._res.writeHeader(header, val);
361
+ }
362
+ } else {
363
+ this._res.writeHeader(header, value);
364
+ }
365
+ }
366
+ this.headersSent = true;
367
+ }
368
+
369
+ /**
370
+ * What node calls before writing a body when the caller never called writeHead. Here there is
371
+ * nothing to flush, since the headers are written with the body, so this only fixes the status.
372
+ */
373
+ _implicitHeader() {
374
+ // compatibility function
375
+ // usually should send headers but this is useless for us
376
+ this.writeHead(this.statusCode);
377
+ }
378
+
379
+ /**
380
+ * Sets the status code.
381
+ * @param {number} code an integer from 100 to 999
382
+ * @returns {this} the response, for chaining
383
+ * @throws {TypeError} if the code is not an integer, "200" included
384
+ * @throws {RangeError} if it is an integer outside the range
385
+ */
386
+ status(code) {
387
+ // Express 5 rejects anything that is not a plausible status code, instead of writing NaN or
388
+ // a nonsense number into the response line, and it tells the two ways of being wrong apart:
389
+ // the wrong type is a TypeError and the wrong number is a RangeError. Both messages are
390
+ // Express's own, since they are what reaches whoever catches them
391
+ if (!Number.isInteger(code)) {
392
+ throw new TypeError(`Invalid status code: ${JSON.stringify(code)}. Status code must be an integer.`);
393
+ }
394
+ if (code < 100 || code > 999) {
395
+ throw new RangeError(
396
+ `Invalid status code: ${JSON.stringify(code)}. Status code must be greater than 99 and less than 1000.`
397
+ );
398
+ }
399
+ this.statusCode = code;
400
+ return this;
401
+ }
402
+
403
+ /**
404
+ * Sets the status and sends its standard message as the body, so 404 answers "Not Found".
405
+ * @param {number} code
406
+ * @returns {this}
407
+ */
408
+ sendStatus(code) {
409
+ return this.status(code)
410
+ .type("txt")
411
+ .send(statuses.message[code] || String(code));
412
+ }
413
+
414
+ /**
415
+ * @param {any} [data]
416
+ * @param {any} [cb]
417
+ * @returns {this}
418
+ */
419
+ end(data, cb) {
420
+ if (typeof data === "function") {
421
+ cb = data;
422
+ data = undefined;
423
+ }
424
+ if (typeof cb !== "function") {
425
+ cb = undefined; // silence the error?
426
+ }
427
+
428
+ if (this.writingChunk) {
429
+ this.once("drain", () => {
430
+ this.end(data, cb);
431
+ });
432
+ return this;
433
+ }
434
+ if (this.finished) {
435
+ return this;
436
+ }
437
+ this.writeHead(this.statusCode);
438
+ this._res.cork(() => {
439
+ if (!this.headersSent) {
440
+ // freshness is not decided here. node's end() knows nothing about conditional
441
+ // requests, and Express answers 304 from send() and from sendFile(), each of
442
+ // which strips the entity headers first. Deciding it here meant res.end("body")
443
+ // answered 304 and dropped the body that the caller had just written.
444
+ const statusMessage = this.statusText ?? statuses.message[this.statusCode] ?? "";
445
+ this._res.writeStatus(`${this.statusCode} ${statusMessage}`.trim());
446
+ this.writeHeaders(true);
447
+ }
448
+ const contentLength = this.headers["content-length"];
449
+ if (STATUSES_WITHOUT_BODY.has(this.statusCode) || this.statusCode < 200) {
450
+ // no body and no length describing one, whatever the caller passed. node decides
451
+ // this the same way, from the status alone, so res.status(304).end("x") sends the
452
+ // status and nothing else on either.
453
+ this._res.endWithoutBody();
454
+ } else if (!data && contentLength) {
455
+ this._res.endWithoutBody(contentLength.toString());
456
+ } else {
457
+ if (data instanceof Buffer) {
458
+ data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
459
+ }
460
+ if (this.req.method === "HEAD") {
461
+ const length = Buffer.byteLength(data ?? "");
462
+ this._res.endWithoutBody(length.toString());
463
+ } else {
464
+ this._res.end(data);
465
+ }
466
+ }
467
+
468
+ this.finished = true;
469
+ this.#socket?.emit("close");
470
+ this.emit("finish");
471
+ this.emit("close");
472
+ cb &&
473
+ queueMicrotask(() => {
474
+ this.#ended = true;
475
+ cb();
476
+ });
477
+ });
478
+ return this;
479
+ }
480
+
481
+ /**
482
+ * Sends the body, picking a Content-Type when none was set and adding an ETag when the
483
+ * "etag" setting asks for one.
484
+ *
485
+ * A number is a value to serialise, the same as a boolean or an object, and never a status
486
+ * code: use `sendStatus()` for that.
487
+ *
488
+ * @param {string|number|boolean|object|Buffer|null} [body]
489
+ * @returns {this}
490
+ */
491
+ send(body) {
492
+ if (this.headersSent) {
493
+ throw new Error("Can't write body: Response was already sent");
494
+ }
495
+ // a typed array is bytes to send, not an object to serialise: res.send(new Uint8Array([104,
496
+ // 101, 121])) is "hey" and not {"0":104,"1":101,"2":121}. Uint8Array and not every view
497
+ // over an ArrayBuffer, because that is what node's own write accepts, and Express hands the
498
+ // view straight to it: a DataView reaches node there and comes out as an empty body
499
+ if (body instanceof Uint8Array && !Buffer.isBuffer(body)) {
500
+ body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
501
+ }
502
+ const isBuffer = Buffer.isBuffer(body);
503
+ // undefined means nothing was passed, and Express treats that differently from a value
504
+ // that happens to be empty: no content-type and no ETag for send(), both for send(null)
505
+ // and send("").
506
+ if (body === undefined) {
507
+ return this.end("");
508
+ }
509
+ // null is an object as far as Express's switch is concerned, so it becomes the empty
510
+ // string without ever reaching the branch that gives a string its content-type. It still
511
+ // earns an ETag. send("") takes the string branch and does get one.
512
+ let skipContentType = false;
513
+ if (body === null) {
514
+ body = "";
515
+ skipContentType = true;
516
+ } else if (typeof body === "object" && !isBuffer) {
517
+ return this.json(body);
518
+ } else if (typeof body === "number") {
519
+ // a number is a value to serialise, the same as a boolean, and never a status code.
520
+ // res.sendStatus() is what sets a status.
521
+ return this.json(body);
522
+ } else if (typeof body === "boolean") {
523
+ return this.json(body);
524
+ } else if (!isBuffer) {
525
+ body = String(body);
526
+ }
527
+ if (typeof body === "string" && !isBuffer) {
528
+ const contentType = this.headers["content-type"];
529
+ if (!contentType) {
530
+ // send(null) sends an empty string without choosing a type. Only a string argument
531
+ // reaches for text/html, which is the branch Express's switch takes for it.
532
+ if (!skipContentType) {
533
+ this.headers["content-type"] = "text/html; charset=utf-8";
534
+ }
535
+ } else if (typeof contentType === "string") {
536
+ // replaced, not only added: the body goes out as utf-8, so a content-type saying
537
+ // iso-8859-1 would be describing bytes that are not there.
538
+ this.headers["content-type"] = withUtf8Charset(contentType);
539
+ }
540
+ } else {
541
+ if (!this.headers["content-type"]) {
542
+ this.headers["content-type"] = "application/octet-stream";
543
+ }
544
+ }
545
+ // the ETag belongs here rather than in end(): node's end() does not produce one, so
546
+ // res.end() and res.redirect() must not either. It has to be set before end() reads
547
+ // req.fresh, which compares If-None-Match against it.
548
+ // body is defined by the time it gets here, so an empty one still earns an ETag. Testing
549
+ // its truthiness instead meant send("") and send(null) came back without one.
550
+ const etagFn = this.app.get("etag fn");
551
+ if (etagFn && !this.headers["etag"] && !this.req.noEtag) {
552
+ const etag = etagFn(body);
553
+ // an application's own etag function is allowed to decline: returning nothing means no
554
+ // header, rather than a header saying "undefined"
555
+ if (etag) {
556
+ this.headers["etag"] = etag;
557
+ }
558
+ }
559
+ // after the ETag, never before: freshness compares If-None-Match against the one that is
560
+ // about to be sent, so a generated ETag has to exist by now.
561
+ if (this.req.fresh) {
562
+ this.status(304);
563
+ }
564
+ // A 204 and a 304 carry no body, so the headers describing one have no meaning and are
565
+ // dropped. A 205 carries no body either but has to say so with an explicit length.
566
+ if (this.statusCode === 204 || this.statusCode === 304) {
567
+ delete this.headers["content-type"];
568
+ delete this.headers["content-length"];
569
+ delete this.headers["transfer-encoding"];
570
+ body = "";
571
+ } else if (this.statusCode === 205) {
572
+ this.headers["content-length"] = "0";
573
+ delete this.headers["transfer-encoding"];
574
+ body = "";
575
+ }
576
+ return this.end(body);
577
+ }
578
+
579
+ /**
580
+ * Streams a file, setting Content-Type from the extension and answering conditional and range
581
+ * requests. The path must be absolute unless `options.root` is given, and a function in the
582
+ * options position is the callback.
583
+ *
584
+ * Options: `root`, `maxAge`, `lastModified`, `headers`, `dotfiles` ("allow", "deny" or
585
+ * "ignore"), `acceptRanges`, `cacheControl`, `immutable`, `etag` and `setHeaders`.
586
+ *
587
+ * @param {string} path
588
+ * @param {Record<string, any>} [options]
589
+ * @param {(err?: Error) => void} [callback] called once sent, or with the error
590
+ */
591
+ sendFile(path, options = new NullObject(), callback) {
592
+ if (!path) {
593
+ throw new TypeError("path argument is required to res.sendFile");
594
+ }
595
+ // a separate message from the one above, as Express has: "required" is wrong for an
596
+ // argument that was passed and was a number
597
+ if (typeof path !== "string") {
598
+ throw new TypeError("path must be a string to res.sendFile");
599
+ }
600
+ if (typeof options === "function") {
601
+ callback = /** @type {any} */ (options);
602
+ options = new NullObject();
603
+ }
604
+ if (!options) options = new NullObject();
605
+ // the callback is optional: without one, errors go to next(). The router assigns req.next
606
+ // before any handler can run, so by the time sendFile is reachable it is always there.
607
+ const done = /** @type {(err?: Error) => void} */ (callback ?? this.req.next);
608
+ // default options
609
+ // Normalised the way send does, and it is not fussiness: max-age takes a non-negative
610
+ // integer count of seconds, so 0.5, -1 and Infinity are all invalid, and a client that
611
+ // cannot read the directive may throw away the whole Cache-Control header with it. A
612
+ // fractional maxAge came out as "max-age=0.5" here, a negative one as "max-age=-1" and
613
+ // Infinity as "max-age=Infinity".
614
+ // Number() around the lot, and not only around the branch that is already a number: ms()
615
+ // answers undefined for a duration it cannot read, and Number.isNaN(undefined) is false,
616
+ // so an unreadable string reached the header as "max-age=NaN".
617
+ const maxAge = Number(
618
+ typeof options.maxAge === "string" ? ms(/** @type {any} */ (options.maxAge)) : options.maxAge
619
+ );
620
+ options.maxAge = Number.isNaN(maxAge) ? 0 : Math.min(Math.max(0, maxAge), MAX_MAXAGE);
621
+ if (typeof options.lastModified === "undefined") {
622
+ options.lastModified = true;
623
+ }
624
+ if (typeof options.cacheControl === "undefined") {
625
+ options.cacheControl = true;
626
+ }
627
+ if (typeof options.acceptRanges === "undefined") {
628
+ options.acceptRanges = true;
629
+ }
630
+ // Express wires the app's setting straight into send here and drops whatever the caller
631
+ // passed, so res.sendFile(p, { etag: false }) still sends one while the app has ETags on.
632
+ // express.static is the opposite: serve-static never asks the app, so a static file keeps
633
+ // its ETag even under app.set("etag", false). It says so with _ownEtag.
634
+ if (!options._ownEtag) {
635
+ options.etag = this.app.get("etag") !== false;
636
+ }
637
+
638
+ // path checks
639
+ if (!options.root && !isAbsolute(path)) {
640
+ // thrown rather than reported to the callback, as Express throws it. A relative path
641
+ // with no root is the calling code being wrong, not the request, and there is nothing
642
+ // the caller's error branch could usefully do with it.
643
+ throw new TypeError("path must be absolute or specify root to res.sendFile");
644
+ }
645
+ if (!options.skipEncodePath) {
646
+ path = encodeURI(path);
647
+ }
648
+ // decode reports failure with -1 rather than throwing, so it needs its own binding before
649
+ // it can go back into path
650
+ const decoded = decode(path);
651
+ if (decoded === -1) {
652
+ this.status(400);
653
+ return done(httpError(400));
654
+ }
655
+ path = decoded;
656
+ if (~path.indexOf("\0")) {
657
+ this.status(400);
658
+ return done(httpError(400));
659
+ }
660
+ if (UP_PATH_REGEXP.test(path)) {
661
+ this.status(403);
662
+ return done(httpError(403));
663
+ }
664
+ const parts = Path.normalize(path).split(Path.sep);
665
+ const fullpath = options.root ? Path.resolve(Path.join(options.root, path)) : path;
666
+ if (options.root && !fullpath.startsWith(Path.resolve(options.root))) {
667
+ this.status(403);
668
+ return done(httpError(403));
669
+ }
670
+
671
+ // dotfile checks
672
+ if (containsDotFile(parts)) {
673
+ switch (options.dotfiles) {
674
+ case "allow":
675
+ break;
676
+ case "deny":
677
+ this.status(403);
678
+ return done(httpError(403));
679
+ case "ignore_files": {
680
+ const len = parts.length;
681
+ if (len > 1 && parts[len - 1].startsWith(".")) {
682
+ this.status(404);
683
+ return done(httpError(404));
684
+ }
685
+ break;
686
+ }
687
+ case "ignore":
688
+ default:
689
+ this.status(404);
690
+ return done(httpError(404));
691
+ }
692
+ }
693
+
694
+ let stat = options._stat;
695
+ if (!stat) {
696
+ try {
697
+ stat = fs.statSync(fullpath);
698
+ } catch (err) {
699
+ // the fs error itself, carrying its errno and path, with send's status written on
700
+ // it: a missing file is the request's 404, an unreadable one is the server's 500
701
+ return done(asStatError(/** @type {any} */ (err)));
702
+ }
703
+ if (stat.isDirectory()) {
704
+ // Express reports a directory as an EISDIR with no status, because send tells it
705
+ // apart from an error: it emits "directory", and res.sendFile has no listener for
706
+ // one. So this is not a 404, and an error handler reading err.code sees the code
707
+ // it expects.
708
+ this.status(404);
709
+ const err = /** @type {any} */ (new Error("EISDIR, read"));
710
+ err.code = "EISDIR";
711
+ return done(err);
712
+ }
713
+ }
714
+
715
+ // headers
716
+ if (!this.headers["content-type"]) {
717
+ const m = mime.lookup(fullpath);
718
+ if (m) this.type(m);
719
+ else this.type("application/octet-stream");
720
+ }
721
+ if (options.cacheControl) {
722
+ this.headers["cache-control"] =
723
+ `public, max-age=${Math.floor(options.maxAge / 1000)}` + (options.immutable ? ", immutable" : "");
724
+ }
725
+ if (options.lastModified) {
726
+ this.headers["last-modified"] = stat.mtime.toUTCString();
727
+ }
728
+ if (options.headers) {
729
+ for (const header in options.headers) {
730
+ // setHeader, not set: Express hands these to send, which writes them through node's
731
+ // setHeader, so a Content-Type given here is written exactly as given. res.set would
732
+ // append a charset and turn "text/x-custom" into "text/x-custom; charset=utf-8",
733
+ // which is a different media type from the one the caller asked for.
734
+ this.setHeader(header, options.headers[header]);
735
+ }
736
+ }
737
+ if (options.setHeaders) {
738
+ options.setHeaders(/** @type {any} */ (this), fullpath, stat);
739
+ }
740
+
741
+ // etag, from the stat and never from the app's "etag fn". send computes this itself with
742
+ // the etag package, so neither a custom fn nor app.set("etag", "strong") reaches a file's
743
+ // ETag on Express either.
744
+ if (options.etag && !this.headers["etag"]) {
745
+ this.headers["etag"] = statTag(stat, true);
746
+ }
747
+ if (!options.etag) {
748
+ this.req.noEtag = true;
749
+ }
750
+
751
+ // announced before the conditional checks, because those can return early and the header
752
+ // still belongs on the response. send does it in the same order, so a 412 or a 416 still
753
+ // tells the client that ranges are available.
754
+ if (options.acceptRanges) {
755
+ this.headers["accept-ranges"] = "bytes";
756
+ }
757
+
758
+ // conditional requests
759
+ if (isPreconditionFailure(this.req, this)) {
760
+ this.status(412);
761
+ return done(httpError(412));
762
+ }
763
+
764
+ // range requests
765
+ let offset = 0,
766
+ len = stat.size,
767
+ ranged = false;
768
+ if (options.acceptRanges) {
769
+ if (this.req.headers.range) {
770
+ let ranges = this.req.range(stat.size, {
771
+ combine: true
772
+ });
773
+
774
+ // if-range
775
+ if (!isRangeFresh(this.req, this)) {
776
+ ranges = -2;
777
+ }
778
+
779
+ if (ranges === -1) {
780
+ this.status(416);
781
+ this.headers["content-range"] = `bytes */${stat.size}`;
782
+ return done(httpError(416));
783
+ }
784
+ if (ranges !== -2 && ranges.length === 1) {
785
+ this.status(206);
786
+ const range = ranges[0];
787
+ this.headers["content-range"] = `bytes ${range.start}-${range.end}/${stat.size}`;
788
+ offset = range.start;
789
+ len = range.end - range.start + 1;
790
+ ranged = true;
791
+ }
792
+ }
793
+ }
794
+
795
+ // if-modified-since, if-none-match
796
+ if (this.req.fresh) {
797
+ // the same fields send removes: everything describing a body that is not being sent.
798
+ // Content-Range goes too, since a 304 answers the whole conditional request and not
799
+ // the range that was asked for.
800
+ delete this.headers["content-type"];
801
+ delete this.headers["content-encoding"];
802
+ delete this.headers["content-language"];
803
+ delete this.headers["content-length"];
804
+ delete this.headers["content-range"];
805
+ this.status(304);
806
+ return this.end();
807
+ }
808
+
809
+ if (this.req.method === "HEAD") {
810
+ this.set("Content-Length", stat.size);
811
+ return this.end();
812
+ }
813
+
814
+ // serve smaller files using workers
815
+ if (this.app.workers.length && stat.size < 768 * 1024 && !ranged) {
816
+ this.app
817
+ .readFileWithWorker(fullpath)
818
+ .then((data) => {
819
+ if (this._res.finished) {
820
+ return;
821
+ }
822
+ this.end(data);
823
+ if (callback) callback();
824
+ })
825
+ .catch((err) => {
826
+ if (callback) callback(err);
827
+ });
828
+ } else {
829
+ // larger files or range requests are piped over response
830
+ const opts = {
831
+ highWaterMark: HIGH_WATERMARK
832
+ };
833
+ if (ranged) {
834
+ opts.start = offset;
835
+ opts.end = Math.max(offset, offset + len - 1);
836
+ }
837
+ const file = fs.createReadStream(fullpath, opts);
838
+ this.set("Content-Length", len);
839
+ file.pipe(this);
840
+ }
841
+ }
842
+
843
+ /**
844
+ * Sends a file as an attachment, so the browser saves it instead of displaying it.
845
+ *
846
+ * `filename` and `options` can both be left out, and a function in either position is taken
847
+ * as the callback.
848
+ *
849
+ * @param {string} path
850
+ * @param {string} [filename] name offered to the user, defaults to the basename of the path
851
+ * @param {Record<string, any>} [options] passed through to sendFile
852
+ * @param {(err?: Error) => void} [callback]
853
+ */
854
+ download(path, filename, options, callback) {
855
+ let done = callback;
856
+ /** @type {string|null|undefined} */
857
+ let name = filename;
858
+ let opts = options || new NullObject();
859
+
860
+ // support function as second or third arg
861
+ if (typeof filename === "function") {
862
+ done = /** @type {any} */ (filename);
863
+ name = null;
864
+ opts = {};
865
+ } else if (typeof options === "function") {
866
+ done = /** @type {any} */ (options);
867
+ opts = {};
868
+ }
869
+
870
+ // support optional filename, where options may be in it's place
871
+ if (typeof filename === "object" && (typeof options === "function" || options === undefined)) {
872
+ name = null;
873
+ opts = filename;
874
+ }
875
+ if (!name) {
876
+ name = Path.basename(path);
877
+ }
878
+ if (!opts.root && !isAbsolute(path)) {
879
+ opts.root = process.cwd();
880
+ }
881
+
882
+ this.attachment(name);
883
+ this.sendFile(path, opts, done);
884
+ }
885
+
886
+ /**
887
+ * Sets a header, node's way: no charset is added to a content-type, since node does not know
888
+ * what a media type is. res.set does that, and is what Express code should use.
889
+ *
890
+ * @param {string} field
891
+ * @param {any} value an array sends the header once per entry
892
+ * @returns {this}
893
+ * @throws {Error} once the headers have gone out
894
+ * @throws {TypeError} if the name is not a string
895
+ */
896
+ setHeader(field, value) {
897
+ if (this.headersSent) {
898
+ throw new Error("Cannot set headers after they are sent to the client");
899
+ }
900
+ if (typeof field !== "string") {
901
+ throw new TypeError("Header name must be a valid HTTP token");
902
+ } else {
903
+ field = field.toLowerCase();
904
+ if (Array.isArray(value)) {
905
+ this.headers[field] = value;
906
+ return this;
907
+ }
908
+ this.headers[field] = String(value);
909
+ }
910
+ return this;
911
+ }
912
+
913
+ /**
914
+ * Node asks this before validating a header value, and answering true keeps it permissive.
915
+ * Only reached through code that goes down node's own header path.
916
+ */
917
+ _isLenientHeaderValidation() {
918
+ // Node.js internal function for lenient header validation
919
+ // Returns true to allow more permissive header value validation
920
+ return true;
921
+ }
922
+
923
+ /**
924
+ * The Express name for set(), including the charset it adds to a content-type.
925
+ * @param {any} field a header name, or an object of them
926
+ * @param {any} [value]
927
+ * @returns {this}
928
+ */
929
+ header(field, value) {
930
+ return this.set(field, value);
931
+ }
932
+
933
+ /**
934
+ * Sets one header, or several from an object. Also available as `header()`.
935
+ * @param {string|object} field header name, or an object of them
936
+ * @param {string|string[]} [value]
937
+ * @returns {this}
938
+ */
939
+ set(field, value) {
940
+ if (typeof field === "object") {
941
+ for (const header in field) {
942
+ // through set() and not straight to setHeader, so that a whole object of headers
943
+ // gets the same coercion and the same Content-Type handling as one set at a time
944
+ this.set(header, field[header]);
945
+ }
946
+ } else {
947
+ field = field.toLowerCase();
948
+ // a header is text on the wire whatever it was here, and Express coerces at this point,
949
+ // so res.get answers what was sent rather than the number or object it was given
950
+ let out = Array.isArray(value) ? value.map(String) : String(value);
951
+ if (field === "content-type") {
952
+ if (Array.isArray(out)) {
953
+ throw new TypeError("Content-Type cannot be set to an Array");
954
+ }
955
+ // every type the mime database gives a charset, not a list of three. The list was
956
+ // missing application/manifest+json among others, which Express does charset.
957
+ out = withDefaultCharset(out);
958
+ }
959
+ this.setHeader(field, out);
960
+ }
961
+ return this;
962
+ }
963
+
964
+ /**
965
+ * Reads a response header that has been set, case insensitively.
966
+ * @param {string} field
967
+ * @returns {string|string[]|undefined}
968
+ */
969
+ get(field) {
970
+ return this.headers[field.toLowerCase()];
971
+ }
972
+
973
+ /**
974
+ * Reads a header that has been set, case insensitively. node's name for get().
975
+ * @param {string} field
976
+ * @returns {string|string[]|undefined}
977
+ */
978
+ getHeader(field) {
979
+ return this.get(field);
980
+ }
981
+
982
+ /**
983
+ * Every header set so far, as the object they are kept in rather than a copy, so writing to
984
+ * it writes to the response.
985
+ * @returns {Record<string, any>}
986
+ */
987
+ getHeaders() {
988
+ return this.headers;
989
+ }
990
+
991
+ /**
992
+ * Removes a header that has not been flushed yet.
993
+ *
994
+ * Returns nothing, the way node's OutgoingMessage does. Returning the response would let
995
+ * chains be written here that break the moment the same code runs on Express.
996
+ *
997
+ * @param {string} field
998
+ */
999
+ removeHeader(field) {
1000
+ delete this.headers[field.toLowerCase()];
1001
+ }
1002
+
1003
+ /**
1004
+ * Adds a header without replacing what is already there, which is what Set-Cookie and Vary
1005
+ * need.
1006
+ * @param {string} field
1007
+ * @param {string|string[]} value
1008
+ * @returns {this}
1009
+ */
1010
+ append(field, value) {
1011
+ field = field.toLowerCase();
1012
+ const old = this.headers[field];
1013
+ if (old) {
1014
+ const newVal = [];
1015
+ if (Array.isArray(old)) {
1016
+ newVal.push(...old);
1017
+ } else {
1018
+ newVal.push(old);
1019
+ }
1020
+ if (Array.isArray(value)) {
1021
+ newVal.push(...value);
1022
+ } else {
1023
+ newVal.push(value);
1024
+ }
1025
+ this.headers[field] = newVal;
1026
+ } else {
1027
+ this.headers[field] = value;
1028
+ }
1029
+ return this;
1030
+ }
1031
+
1032
+ /**
1033
+ * Renders a view and sends it. With a callback the result goes to the callback instead, and
1034
+ * nothing is sent. A function in the options position is taken as the callback.
1035
+ * @param {string} view view name
1036
+ * @param {Record<string, any>} [options] locals for the view
1037
+ * @param {(err: Error|null, html?: string) => void} [callback]
1038
+ */
1039
+ render(view, options, callback) {
1040
+ if (typeof options === "function") {
1041
+ callback = /** @type {any} */ (options);
1042
+ options = {};
1043
+ }
1044
+ if (!options) {
1045
+ options = {};
1046
+ } else {
1047
+ options = Object.assign({}, options);
1048
+ }
1049
+ options._locals = this.locals;
1050
+ const done =
1051
+ callback ||
1052
+ ((err, str) => {
1053
+ if (err) return this.req.next(err);
1054
+ this.send(str);
1055
+ });
1056
+
1057
+ // use req.app like express does, so mounted sub-apps resolve views with their own settings
1058
+ this.req.app.render(view, options, done);
1059
+ }
1060
+
1061
+ /**
1062
+ * Appends a Set-Cookie header. An object value is serialised as JSON. With `signed` the
1063
+ * cookie is signed using the secret given to cookie-parser.
1064
+ * @param {string} name
1065
+ * @param {string|object} value
1066
+ * @param {{maxAge?: number, expires?: Date, path?: string, domain?: string, secure?: boolean,
1067
+ * httpOnly?: boolean, sameSite?: boolean|"lax"|"strict"|"none", signed?: boolean,
1068
+ * priority?: "low"|"medium"|"high", partitioned?: boolean}} [options]
1069
+ * @returns {this}
1070
+ */
1071
+ cookie(name, value, options) {
1072
+ const opt = { ...(options ?? {}) }; // create a new ref because we change original object (https://github.com/dimdenGD/ultimate-express/issues/68)
1073
+ if (opt.signed && !this.req.secret) {
1074
+ // the message has to read like this: it is the one Express throws, and it names the
1075
+ // thing that is actually missing rather than the library that noticed
1076
+ throw new Error('cookieParser("secret") required for signed cookies');
1077
+ }
1078
+ let val = typeof value === "object" ? "j:" + JSON.stringify(value) : String(value);
1079
+ if (opt.maxAge != null) {
1080
+ const maxAge = opt.maxAge - 0;
1081
+ if (!isNaN(maxAge)) {
1082
+ opt.expires = new Date(Date.now() + maxAge);
1083
+ opt.maxAge = Math.floor(maxAge / 1000);
1084
+ }
1085
+ } else {
1086
+ // Express carries a null maxAge through to a cookie package that ignores it. Ours
1087
+ // refuses it, so it is dropped here instead: no Max-Age is what both end up sending
1088
+ delete opt.maxAge;
1089
+ }
1090
+ if (opt.signed) {
1091
+ val = "s:" + sign(val, this.req.secret);
1092
+ }
1093
+
1094
+ if (opt.path == null) {
1095
+ opt.path = "/";
1096
+ }
1097
+
1098
+ this.append("Set-Cookie", cookie.serialize(name, val, opt));
1099
+ return this;
1100
+ }
1101
+
1102
+ /**
1103
+ * Clears a cookie. The browser only matches it if `path` and `domain` are the ones it was
1104
+ * set with. Any `maxAge` or `expires` passed here is ignored, since clearing is defined as
1105
+ * expiring it immediately.
1106
+ * @param {string} name
1107
+ * @param {Record<string, any>} [options]
1108
+ * @returns {this}
1109
+ */
1110
+ clearCookie(name, options) {
1111
+ // clearing is defined as expiring now, so any maxAge passed in is dropped rather than honoured
1112
+ /** @type {Record<string, any>} */
1113
+ const opts = { path: "/", ...options, expires: new Date(1) };
1114
+ delete opts.maxAge;
1115
+ return this.cookie(name, "", opts);
1116
+ }
1117
+
1118
+ /**
1119
+ * Sets Content-Disposition to attachment, and Content-Type from the extension when a
1120
+ * filename is given.
1121
+ * @param {string} [filename]
1122
+ * @returns {this}
1123
+ */
1124
+ attachment(filename) {
1125
+ if (filename) {
1126
+ this.type(Path.extname(filename));
1127
+ }
1128
+ this.set("Content-Disposition", contentDisposition(filename));
1129
+ return this;
1130
+ }
1131
+
1132
+ /**
1133
+ * Answers according to the Accept header, calling the handler whose key matches best. A
1134
+ * `default` key catches everything else; without one an unmatched request gets 406.
1135
+ * Sets Vary: Accept.
1136
+ * @param {Record<string, any>} object handlers keyed by extension or mime type
1137
+ * @returns {this}
1138
+ */
1139
+ format(object) {
1140
+ const keys = Object.keys(object).filter((v) => v !== "default");
1141
+ const key = keys.length > 0 ? this.req.accepts(keys) : false;
1142
+
1143
+ this.vary("Accept");
1144
+
1145
+ if (key) {
1146
+ this.set("Content-Type", normalizeType(key).value);
1147
+ object[key](this.req, this, this.req.next);
1148
+ } else if (object.default) {
1149
+ object.default(this.req, this, this.req.next);
1150
+ } else {
1151
+ this.status(406).send(this.app._generateErrorPage("Not Acceptable", this.statusCode, false));
1152
+ }
1153
+
1154
+ return this;
1155
+ }
1156
+
1157
+ /**
1158
+ * Sends JSON, honouring the "json replacer", "json spaces" and "json escape" settings.
1159
+ * @param {*} body
1160
+ * @returns {this}
1161
+ */
1162
+ json(body) {
1163
+ if (!this.headers["content-type"]) {
1164
+ this.headers["content-type"] = "application/json; charset=utf-8";
1165
+ }
1166
+ const escape = this.app.get("json escape");
1167
+ const replacer = this.app.get("json replacer");
1168
+ const spaces = this.app.get("json spaces");
1169
+ return this.send(stringify(body, replacer, spaces, escape));
1170
+ }
1171
+
1172
+ /**
1173
+ * Sends JSON wrapped in a callback when the query names one, under the setting
1174
+ * "jsonp callback name", which defaults to "callback". Without it this is plain JSON.
1175
+ * @param {*} object
1176
+ * @returns {this}
1177
+ */
1178
+ jsonp(object) {
1179
+ let callback = this.req.query[this.app.get("jsonp callback name")];
1180
+ let body = stringify(
1181
+ object,
1182
+ this.app.get("json replacer"),
1183
+ this.app.get("json spaces"),
1184
+ this.app.get("json escape")
1185
+ );
1186
+ let js = false;
1187
+
1188
+ if (Array.isArray(callback)) {
1189
+ callback = callback[0];
1190
+ }
1191
+
1192
+ if (typeof callback === "string" && callback.length !== 0) {
1193
+ callback = callback.replace(/[^[\]\w$.]/g, "");
1194
+
1195
+ if (body === undefined) {
1196
+ body = "";
1197
+ } else if (typeof body === "string") {
1198
+ // replace chars not allowed in JavaScript that are in JSON
1199
+ body = body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1200
+ }
1201
+ body = "/**/ typeof " + callback + " === 'function' && " + callback + "(" + body + ");";
1202
+ js = true;
1203
+ }
1204
+
1205
+ if (!this.headers["content-type"]) {
1206
+ this.headers["x-content-type-options"] = "nosniff";
1207
+ this.headers["content-type"] = "application/json; charset=utf-8";
1208
+ }
1209
+ if (js) {
1210
+ // with a callback the body is script whatever type was asked for before, so this
1211
+ // overrides rather than filling in, and the nosniff goes with it
1212
+ this.headers["x-content-type-options"] = "nosniff";
1213
+ this.headers["content-type"] = "text/javascript; charset=utf-8";
1214
+ }
1215
+
1216
+ return this.send(body);
1217
+ }
1218
+
1219
+ /**
1220
+ * Adds to the Link header, one entry per key, the key being the rel.
1221
+ * @param {Record<string, any>} links rel to url
1222
+ * @returns {this}
1223
+ */
1224
+ links(links) {
1225
+ let link = this.get("Link") || "";
1226
+ if (link) link += ", ";
1227
+ return this.set(
1228
+ "Link",
1229
+ link +
1230
+ Object.keys(links)
1231
+ .map(function (rel) {
1232
+ const target = links[rel];
1233
+ // an array is several links that share a rel, one entry each, and not one
1234
+ // entry holding a comma separated list inside its angle brackets
1235
+ if (Array.isArray(target)) {
1236
+ return target.map((one) => "<" + one + '>; rel="' + rel + '"').join(", ");
1237
+ }
1238
+ return "<" + target + '>; rel="' + rel + '"';
1239
+ })
1240
+ .join(", ")
1241
+ );
1242
+ }
1243
+
1244
+ /**
1245
+ * Sets the Location header, URL-encoding the value.
1246
+ *
1247
+ * "back" is a literal location here, not the Referrer: that shortcut is gone in Express 5.
1248
+ *
1249
+ * @param {string} path
1250
+ * @returns {this}
1251
+ */
1252
+ location(path) {
1253
+ // Express 5 dropped the magic where 'back' meant the Referrer header. It is now just a
1254
+ // relative URL like any other, which is what res.redirect('back') also does here.
1255
+ this.headers["location"] = encodeUrl(path);
1256
+ return this;
1257
+ }
1258
+
1259
+ /**
1260
+ * Redirects, defaulting to 302. The status may be given first, as `redirect(301, url)`.
1261
+ * The body is a short note in whichever format the client accepts.
1262
+ * @param {number|string} status status code, or the url when the status is left out
1263
+ * @param {string} [url]
1264
+ * @param {boolean} [forceHtml] answer with an HTML body whatever the client accepts
1265
+ */
1266
+ redirect(status, url, forceHtml = false) {
1267
+ if (typeof status !== "number" && !url) {
1268
+ url = status;
1269
+ status = 302;
1270
+ }
1271
+ this.location(/** @type {string} */ (url));
1272
+ this.status(/** @type {number} */ (status));
1273
+
1274
+ // a string, because location() has just set it to one. get() has to allow the array form for
1275
+ // the headers that can repeat, and escapeHtml quite reasonably only takes a string
1276
+ const address = /** @type {string} */ (this.get("Location"));
1277
+ let body;
1278
+ // Support text/{plain,html} by default
1279
+ if (forceHtml) {
1280
+ // uppercase on purpose: this branch stands in for the redirect that send and
1281
+ // serve-static emit, and both of those write "charset=UTF-8". res.redirect() below
1282
+ // goes through format(), which takes the lowercase form from the mime lookup.
1283
+ this.set("Content-Type", "text/html; charset=UTF-8");
1284
+ body =
1285
+ "<!DOCTYPE html>\n" +
1286
+ '<html lang="en">\n' +
1287
+ "<head>\n" +
1288
+ '<meta charset="utf-8">\n' +
1289
+ "<title>Redirecting</title>\n" +
1290
+ "</head>\n" +
1291
+ "<body>\n" +
1292
+ `<pre>Redirecting to ${escapeHtml(address)}</pre>\n` +
1293
+ "</body>\n" +
1294
+ "</html>\n";
1295
+ } else {
1296
+ this.format({
1297
+ text: () => {
1298
+ this.set("Content-Type", "text/plain; charset=utf-8");
1299
+ body = `${statuses.message[status]}. Redirecting to ${address}`;
1300
+ },
1301
+ html: () => {
1302
+ this.set("Content-Type", "text/html; charset=utf-8");
1303
+ body = `<p>${statuses.message[status]}. Redirecting to ${escapeHtml(address)}</p>`;
1304
+ },
1305
+ default: () => {
1306
+ this.set("Content-Type", "text/plain; charset=utf-8");
1307
+ body = "";
1308
+ }
1309
+ });
1310
+ }
1311
+ if (this.req.method === "HEAD") {
1312
+ this.end();
1313
+ } else {
1314
+ this.end(body);
1315
+ }
1316
+ }
1317
+
1318
+ /**
1319
+ * Sets Content-Type. An extension is looked up as a mime type and gets a charset; anything
1320
+ * containing a slash is used as written. Also available as `contentType()`.
1321
+ * @param {string} type
1322
+ * @returns {this}
1323
+ */
1324
+ type(type) {
1325
+ const ct = type.indexOf("/") === -1 ? contentTypeFor(type) : type;
1326
+
1327
+ return this.set("content-type", ct);
1328
+ }
1329
+
1330
+ contentType = this.type;
1331
+
1332
+ /**
1333
+ * Adds a field to Vary, without repeating one already there.
1334
+ * @param {string|string[]} field
1335
+ * @returns {this}
1336
+ * @throws {TypeError} if no field is given, since a Vary with nothing in it is a mistake
1337
+ */
1338
+ vary(field) {
1339
+ // the vary package decides: it throws when there is no field at all, and does nothing at
1340
+ // all for an empty list, which is not the same thing and used to be refused here as well
1341
+ vary(/** @type {any} */ (this), field);
1342
+ return this;
1343
+ }
1344
+
1345
+ /** The same object as socket, which node carries under both names. */
1346
+ get connection() {
1347
+ return this.socket;
1348
+ }
1349
+
1350
+ /**
1351
+ * Whether the response has been fully written. Writable declares this as a plain property and
1352
+ * the machinery that would maintain it is bypassed here, so a getter over our own flag replaces
1353
+ * it. The directive below has to sit outside this block, or it reads as a JSDoc tag.
1354
+ */
1355
+ // @ts-expect-error TS2611, the accessor replacing the base property is deliberate. Expect
1356
+ // rather than ignore, so it fails loudly if it ever stops applying.
1357
+ get writableFinished() {
1358
+ return this.finished;
1359
+ }
1360
+ };