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.
package/src/request.js ADDED
@@ -0,0 +1,807 @@
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 { patternToRegex, deprecated, NullObject } = require("./utils.js");
19
+ const accepts = require("accepts");
20
+ const typeis = require("type-is");
21
+ const parseRange = require("range-parser");
22
+ const proxyaddr = require("proxy-addr");
23
+ const { isIP } = require("node:net");
24
+ const fresh = require("fresh");
25
+ const { Readable } = require("stream");
26
+
27
+ // accepts, type-is, proxy-addr and fresh all declare a node IncomingMessage and read nothing off
28
+ // it but .headers. This request is deliberately not one, so it is handed over as itself and the
29
+ // declared shape is stepped around at each call.
30
+ const asMessage = (req) => /** @type {any} */ (req);
31
+
32
+ /**
33
+ * Writes an address the way node writes socket.remoteAddress, which is inet_ntop's output and so
34
+ * RFC 5952: leading zeros dropped from each group, the longest run of two or more zero groups
35
+ * written as "::", and the last four bytes written in dotted form for the addresses that carry an
36
+ * IPv4 one. uWS hands over the sixteen bytes, and writing them out in full gave req.ip
37
+ * "0000:0000:0000:0000:0000:0000:0000:0001" where Express says "::1".
38
+ *
39
+ * @param {number[]} groups the eight 16-bit groups, most significant first
40
+ * @returns {string}
41
+ */
42
+ function formatIPv6(groups) {
43
+ // longest run of zero groups, leftmost on a tie, which is the run inet_ntop replaces
44
+ let bestStart = -1;
45
+ let bestLength = 0;
46
+ for (let i = 0; i < 8; i++) {
47
+ if (groups[i] !== 0) continue;
48
+ let run = 1;
49
+ while (i + run < 8 && groups[i + run] === 0) run++;
50
+ if (run > bestLength) {
51
+ bestStart = i;
52
+ bestLength = run;
53
+ }
54
+ i += run - 1;
55
+ }
56
+ // a single zero group is written as "0", not as "::"
57
+ if (bestLength < 2) {
58
+ bestStart = -1;
59
+ bestLength = 0;
60
+ }
61
+
62
+ // ::ffff:a.b.c.d, and the deprecated ::a.b.c.d. The test is inet_ntop's own, including that a
63
+ // run of seven leading zeros never reaches it, since group 6 is inside the run by then.
64
+ const mixed =
65
+ bestStart === 0 &&
66
+ (bestLength === 6 || (bestLength === 7 && groups[7] !== 1) || (bestLength === 5 && groups[5] === 0xffff));
67
+
68
+ let out = "";
69
+ for (let i = 0; i < 8; i++) {
70
+ if (bestStart !== -1 && i >= bestStart && i < bestStart + bestLength) {
71
+ if (i === bestStart) out += ":";
72
+ continue;
73
+ }
74
+ if (i !== 0) out += ":";
75
+ if (mixed && i === 6) {
76
+ out += `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
77
+ break;
78
+ }
79
+ out += groups[i].toString(16);
80
+ }
81
+ // a run reaching the end leaves a trailing group to close the "::"
82
+ if (bestStart !== -1 && bestStart + bestLength === 8) out += ":";
83
+ return out;
84
+ }
85
+
86
+ const discardedDuplicates = new Set([
87
+ "age",
88
+ "authorization",
89
+ "content-length",
90
+ "content-type",
91
+ "etag",
92
+ "expires",
93
+ "from",
94
+ "host",
95
+ "if-modified-since",
96
+ "if-unmodified-since",
97
+ "last-modified",
98
+ "location",
99
+ "max-forwards",
100
+ "proxy-authorization",
101
+ "referer",
102
+ "retry-after",
103
+ "server",
104
+ "user-agent"
105
+ ]);
106
+
107
+ let key = 0;
108
+
109
+ // 128 KB of body buffered before uWS is asked to pause
110
+ const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
111
+
112
+ module.exports = class Request extends Readable {
113
+ /** @type {Record<string, any>|null} */
114
+ #cachedQuery = null;
115
+
116
+ /** @type {Record<string, any>|null} */
117
+ #cachedHeaders = null;
118
+
119
+ /** @type {Record<string, string[]>|null} */
120
+ #cachedDistinctHeaders = null;
121
+
122
+ // Flat, name then value: an array of pairs meant one array allocated per header on every
123
+ // request, and a request carries eight or ten of them. Everything that reads this walks it two
124
+ // at a time.
125
+ #rawHeadersEntries = [];
126
+
127
+ /** @type {string|undefined|null} */
128
+ #cachedParsedIp = null;
129
+
130
+ #paused = false;
131
+
132
+ body;
133
+
134
+ res;
135
+
136
+ optimizedParams;
137
+
138
+ _error;
139
+
140
+ noEtag;
141
+
142
+ /**
143
+ * Built for every request, which is why so little happens here. The headers are copied out
144
+ * because uWS only lends them for this call, everything derived from them waits until something
145
+ * asks, and the body is subscribed to only for the methods that carry one.
146
+ *
147
+ * @param {any} req the uWS request, readable only during this call
148
+ * @param {any} res the uWS response
149
+ * @param {any} app the application or router this request arrived at
150
+ */
151
+ constructor(req, res, app) {
152
+ // the same object every time: Readable reads these options and never writes to them
153
+ super(READABLE_OPTIONS);
154
+ this._res = res;
155
+ this._req = req;
156
+ this.readable = true;
157
+ this._req.forEach((key, value) => {
158
+ this.#rawHeadersEntries.push(key, value);
159
+ // spotted in the loop that is running anyway: a client asking for the connection to be
160
+ // closed must not be answered that it is being kept alive. The response is built right
161
+ // after this and reads the flag.
162
+ if (key.length === 10 && key === "connection" && value.length === 5 && value.toLowerCase() === "close") {
163
+ this._connectionClose = true;
164
+ }
165
+ });
166
+ this.routeCount = 1;
167
+ this.key = key++;
168
+ if (key > 100000) {
169
+ key = 0;
170
+ }
171
+ this.app = app;
172
+ // both forms are kept, because both are asked for: the query with its "?" goes into
173
+ // req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
174
+ // back off for every request that reads req.query.
175
+ this._rawQuery = req.getQuery() ?? "";
176
+ this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
177
+ // getUrl() is the path already, so the query is joined on and then not split off again.
178
+ // Building originalUrl and picking the path back out of it with indexOf and substring was
179
+ // a search and a second string for something uWS had just handed over.
180
+ this.path = req.getUrl();
181
+ this.originalUrl = this.path + this.urlQuery;
182
+ this.url = this.originalUrl;
183
+ // charCodeAt rather than indexing: s[i] builds a one character string to throw away
184
+ this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
185
+ this._opPath = this.path;
186
+ this._originalPath = this.path;
187
+ if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
188
+ this._opPath = this._opPath.slice(0, -1);
189
+ }
190
+ this.method = req.getCaseSensitiveMethod().toUpperCase();
191
+ this._isOptions = this.method === "OPTIONS";
192
+ this._isHead = this.method === "HEAD";
193
+ this.params = {};
194
+
195
+ // Two Sets per request, for two things almost no request needs.
196
+ //
197
+ // _matchedMethods collects the verbs a path answers so an OPTIONS request can be told what
198
+ // they are, and every place that reads it asks _isOptions first, so it is built only for
199
+ // the requests that are one.
200
+ //
201
+ // _gotParams remembers which app.param() callbacks have already run for this request, so it
202
+ // is only wanted by an application that uses app.param at all. The router builds it the
203
+ // first time it has something to put in it.
204
+ this._matchedMethods = this._isOptions ? new Set() : null;
205
+ this._gotParams = null;
206
+ this._stack = [];
207
+ // number of entries in _stack that aren't the empty path. while this is 0 the whole
208
+ // stack joins to "", so getFullMountpath can skip the join entirely
209
+ this._stackMounted = 0;
210
+ this._paramStack = [];
211
+ this.receivedData = false;
212
+ // reading ip is very slow in UWS, so its better to not do it unless truly needed
213
+ if (this.app.needsIpAfterResponse || this.key < 100) {
214
+ // if app needs ip after response, read it now because after response its not accessible
215
+ // also read it for first 100 requests to not error
216
+ this.rawIp = this._res.getRemoteAddress();
217
+ }
218
+
219
+ const additionalMethods = this.app.get("body methods");
220
+ // skip reading body for non-POST requests
221
+ // this makes it +10k req/sec faster
222
+ if (
223
+ this.method === "POST" ||
224
+ this.method === "PUT" ||
225
+ this.method === "PATCH" ||
226
+ this.method === "QUERY" ||
227
+ (additionalMethods && additionalMethods.includes(this.method))
228
+ ) {
229
+ this._res.onData((ab, isLast) => {
230
+ this.receivedData = true;
231
+ if (this.#responseEnded) {
232
+ return;
233
+ }
234
+ // ab.slice(0) copies the ArrayBuffer; uWS neuters `ab` after this callback,
235
+ // so a Buffer.from(ab) view would corrupt data left in the Readable queue.
236
+ const chunk = Buffer.from(ab.slice(0));
237
+ const accepted = this.push(chunk);
238
+ // push() may synchronously end the response via a flowing-mode listener.
239
+ if (!accepted && !isLast && !this.#responseEnded) {
240
+ this._res.pause();
241
+ this.#paused = true;
242
+ }
243
+ if (isLast) {
244
+ this.push(null);
245
+ }
246
+ });
247
+ } else {
248
+ this.receivedData = true;
249
+ this.push(null);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Whether there is any point still reading the body: once the response is finished or the
255
+ * connection is gone, uWS has nothing left to hand over.
256
+ */
257
+ get #responseEnded() {
258
+ return this.res?.finished || this.res?.aborted;
259
+ }
260
+
261
+ /**
262
+ * Readable's pull. uWS pushes the body rather than being pulled from, so all this does is
263
+ * lift the backpressure that a full queue put on it.
264
+ */
265
+ _read() {
266
+ if (this.#paused && !this.#responseEnded) {
267
+ this.#paused = false;
268
+ this._res.resume();
269
+ }
270
+ }
271
+
272
+ /**
273
+ * The part of the path the routers mounted so far have consumed, which is the empty string at
274
+ * the top level. Matched rather than joined, because a mount path can be a pattern.
275
+ * @returns {string}
276
+ */
277
+ get baseUrl() {
278
+ const match = this._originalPath.match(patternToRegex(this._stack.join(""), true));
279
+ return match ? match[0] : "";
280
+ }
281
+
282
+ /**
283
+ * Only here because a getter without a setter makes the property read-only, and middleware in
284
+ * the wild does assign to it. Express keeps it writable too.
285
+ */
286
+ set baseUrl(x) {
287
+ this._originalPath = x;
288
+ }
289
+
290
+ /**
291
+ * The Host header as sent, trimmed and resolved through trust proxy, port still attached.
292
+ * X-Forwarded-Host wins when the peer is trusted, and only its first entry: the header is
293
+ * meant to carry one value, but nothing stops a proxy from appending.
294
+ */
295
+ get #authority() {
296
+ const trust = this.app.get("trust proxy fn");
297
+ const isTrusted = !!(trust && trust(this.connection.remoteAddress, 0));
298
+ const rawHeader = (isTrusted && this.headers["x-forwarded-host"]) || this.headers["host"];
299
+ let host = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
300
+
301
+ if (typeof host !== "string" || !host) return;
302
+ host = host.trim();
303
+
304
+ if (isTrusted) {
305
+ const commaIndex = host.indexOf(",");
306
+ if (commaIndex !== -1) {
307
+ // Note: X-Forwarded-Host is normally only ever a
308
+ // single value, but this is to be safe.
309
+ host = host.substring(0, commaIndex).trimEnd();
310
+ }
311
+ }
312
+
313
+ return host || undefined;
314
+ }
315
+
316
+ /** The authority with the port removed, taking care not to read an IPv6 literal's colons. */
317
+ get #host() {
318
+ const host = this.#authority;
319
+ if (!host) return;
320
+
321
+ const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0;
322
+ const portIndex = host.indexOf(":", offset);
323
+
324
+ return portIndex !== -1 ? host.substring(0, portIndex) : host;
325
+ }
326
+
327
+ /**
328
+ * The authority, port included, from Host or from X-Forwarded-Host behind a trusted proxy.
329
+ * `hostname` is the same value without the port.
330
+ * @returns {string}
331
+ */
332
+ get host() {
333
+ return this.#authority;
334
+ }
335
+
336
+ /**
337
+ * The host without the port.
338
+ * @returns {string}
339
+ */
340
+ get hostname() {
341
+ return this.#host;
342
+ }
343
+
344
+ /**
345
+ * Always "1.1". uWS speaks HTTP/1.1 and, when built for it, HTTP/3, and reports neither
346
+ * version through this API, so the value node code expects to find here is hardcoded.
347
+ * @returns {string}
348
+ */
349
+ get httpVersion() {
350
+ return "1.1";
351
+ }
352
+
353
+ /** @returns {number} the 1 of HTTP/1.1, for code that reads the parts separately */
354
+ get httpVersionMajor() {
355
+ return 1;
356
+ }
357
+
358
+ /** @returns {number} the second 1 of HTTP/1.1 */
359
+ get httpVersionMinor() {
360
+ return 1;
361
+ }
362
+
363
+ /**
364
+ * The client address. With "trust proxy" set this is the first address in X-Forwarded-For
365
+ * that the trust function accepts, otherwise it is the socket's own address.
366
+ * @returns {string|undefined} undefined on a unix socket, which has no address
367
+ */
368
+ get ip() {
369
+ const trust = this.app.get("trust proxy fn");
370
+ if (!trust) {
371
+ return this.parsedIp;
372
+ }
373
+ return proxyaddr(asMessage(this), trust);
374
+ }
375
+
376
+ /**
377
+ * The trusted addresses from X-Forwarded-For, nearest client first, empty unless
378
+ * "trust proxy" is set.
379
+ * @returns {string[]}
380
+ */
381
+ get ips() {
382
+ const trust = this.app.get("trust proxy fn");
383
+ if (!trust) {
384
+ return [];
385
+ }
386
+ const addrs = proxyaddr.all(asMessage(this), trust);
387
+ addrs.reverse().pop();
388
+ return addrs;
389
+ }
390
+
391
+ /**
392
+ * "http" or "https", taken from X-Forwarded-Proto when the connection comes from a
393
+ * trusted proxy.
394
+ * @returns {string}
395
+ */
396
+ get protocol() {
397
+ const proto = this.app.ssl ? "https" : "http";
398
+ const trust = this.app.get("trust proxy fn");
399
+ if (!trust) {
400
+ return proto;
401
+ }
402
+ if (!trust(this.connection.remoteAddress, 0)) {
403
+ return proto;
404
+ }
405
+ const header = this.headers["x-forwarded-proto"] || proto;
406
+ const index = header.indexOf(",");
407
+
408
+ return index !== -1 ? header.slice(0, index).trim() : header.trim();
409
+ }
410
+
411
+ /**
412
+ * The query string parsed by whichever parser the "query parser" setting names, cached for the
413
+ * life of the request. A null-prototype object, so a key like "__proto__" cannot reach
414
+ * Object.prototype. No setter, so assigning to req.query throws as it does on Express.
415
+ *
416
+ * @returns {Record<string, any>}
417
+ */
418
+ get query() {
419
+ if (this.#cachedQuery) {
420
+ return this.#cachedQuery;
421
+ }
422
+ const qp = this.app.get("query parser fn");
423
+ // copied onto a plain null-prototype object, or node inspects fast-querystring's result as
424
+ // "Empty <[Object: null prototype] {}>" where Express shows "[Object: null prototype]".
425
+ // Object.create(null) and not { __proto__: null }: 318ns against 640
426
+ const parsed = qp ? Object.assign(Object.create(null), qp(this._rawQuery)) : Object.create(null);
427
+ this.#cachedQuery = parsed;
428
+ return parsed;
429
+ }
430
+
431
+ /**
432
+ * Whether the request came in over TLS.
433
+ * @returns {boolean}
434
+ */
435
+ get secure() {
436
+ return this.protocol === "https";
437
+ }
438
+
439
+ /** @type {string[]|null} */
440
+ #cachedSubdomains = null;
441
+
442
+ /**
443
+ * The hostname's subdomains, furthest from the root first, dropping the last
444
+ * "subdomain offset" labels. Empty for an IP address.
445
+ * @returns {string[]}
446
+ */
447
+ get subdomains() {
448
+ if (this.#cachedSubdomains !== null) {
449
+ return this.#cachedSubdomains;
450
+ }
451
+
452
+ const hostname = this.hostname;
453
+ if (!hostname || isIP(hostname)) {
454
+ return (this.#cachedSubdomains = []);
455
+ }
456
+
457
+ const offset = this.app.get("subdomain offset");
458
+ const parts = hostname.split(".");
459
+ const subdomains = parts.reverse().slice(offset);
460
+
461
+ return (this.#cachedSubdomains = subdomains);
462
+ }
463
+
464
+ /**
465
+ * Whether X-Requested-With says XMLHttpRequest. Only libraries that set that header are
466
+ * detected, which today is mostly jQuery and not fetch.
467
+ * @returns {boolean}
468
+ */
469
+ get xhr() {
470
+ const val = this.headers?.["x-requested-with"];
471
+ return typeof val === "string" && val.toLowerCase() === "xmlhttprequest";
472
+ }
473
+
474
+ /**
475
+ * The peer address as text, read from uWS and cached. Reading it is expensive and it is gone
476
+ * once the response has finished, so it is read up front for the first hundred requests, and
477
+ * for every request once an application has been seen asking too late. That app gets 127.0.0.1
478
+ * once and the real address from the next request on.
479
+ *
480
+ * @returns {string|undefined} undefined over a unix socket, which has no address
481
+ */
482
+ get parsedIp() {
483
+ if (this.#cachedParsedIp !== null) {
484
+ return this.#cachedParsedIp;
485
+ }
486
+ const finished = this.res.finished;
487
+ if (finished) {
488
+ // mark app as one that needs ip after response
489
+ this.app.needsIpAfterResponse = true;
490
+ }
491
+ if (!this.rawIp) {
492
+ if (finished) {
493
+ // fallback once
494
+ return "127.0.0.1";
495
+ }
496
+ this.rawIp = this._res.getRemoteAddress();
497
+ }
498
+ /** @type {string|undefined} */
499
+ let ip;
500
+ if (this.rawIp.byteLength === 4) {
501
+ // ipv4
502
+ ip = new Uint8Array(this.rawIp).join(".");
503
+ } else if (this.rawIp.byteLength === 16) {
504
+ // ipv6
505
+ const dv = new DataView(this.rawIp);
506
+ const groups = new Array(8);
507
+ for (let i = 0; i < 8; i++) {
508
+ groups[i] = dv.getUint16(i * 2);
509
+ }
510
+ ip = formatIPv6(groups);
511
+ } else {
512
+ ip = undefined; // unix sockets dont have ip
513
+ }
514
+ this.#cachedParsedIp = ip;
515
+ return ip;
516
+ }
517
+
518
+ /**
519
+ * Enough of a node socket for the middleware that reaches for one. It is built on each read
520
+ * rather than kept, since almost nothing asks for it.
521
+ * @returns {{remoteAddress: string|undefined, remotePort: number, localPort: number|undefined, encrypted: boolean, end: (body?: any) => void}}
522
+ */
523
+ get connection() {
524
+ return {
525
+ remoteAddress: this.parsedIp,
526
+ remotePort: this._res.getRemotePort(),
527
+ localPort: this.app.port,
528
+ encrypted: this.app.ssl,
529
+ end: (body) => this.res.end(body)
530
+ };
531
+ }
532
+
533
+ /**
534
+ * The same object `connection` builds. node carries both names and middleware reaches for
535
+ * either one, so both are here.
536
+ */
537
+ get socket() {
538
+ return this.connection;
539
+ }
540
+
541
+ /**
542
+ * Whether the client's cached copy is still good, from If-None-Match and If-Modified-Since
543
+ * against the response headers set so far. Only GET and HEAD can be fresh.
544
+ * @returns {boolean}
545
+ */
546
+ get fresh() {
547
+ if (this.method !== "HEAD" && this.method !== "GET") {
548
+ return false;
549
+ }
550
+ if ((this.res.statusCode >= 200 && this.res.statusCode < 300) || this.res.statusCode === 304) {
551
+ // fast path: res.send() reads req.fresh on every response it sends, but fresh() can
552
+ // only return true when the request carries a conditional header. Scan the raw entries
553
+ // instead of materializing the full headers object, which is lazy by design.
554
+ // Only valid while headers are untouched: both the getter and the setter populate #cachedHeaders.
555
+ if (this.#cachedHeaders === null) {
556
+ let hasConditional = false;
557
+ const entries = this.#rawHeadersEntries;
558
+ for (let i = 0, len = entries.length; i < len; i += 2) {
559
+ const key = entries[i];
560
+ // 'if-none-match'.length === 13, 'if-modified-since'.length === 17
561
+ if (key.length === 13 || key.length === 17) {
562
+ const lower = key.toLowerCase();
563
+ if (lower === "if-none-match" || lower === "if-modified-since") {
564
+ hasConditional = true;
565
+ break;
566
+ }
567
+ }
568
+ }
569
+ if (!hasConditional) {
570
+ return false;
571
+ }
572
+ }
573
+ return fresh(this.headers, {
574
+ etag: this.res.headers["etag"],
575
+ "last-modified": this.res.headers["last-modified"]
576
+ });
577
+ }
578
+ return false;
579
+ }
580
+
581
+ /**
582
+ * The opposite of `fresh`.
583
+ * @returns {boolean}
584
+ */
585
+ get stale() {
586
+ return !this.fresh;
587
+ }
588
+
589
+ /**
590
+ * Reads a request header, case insensitively. "referer" and "referrer" both work, whichever
591
+ * one the client sent.
592
+ *
593
+ * @param {string} field header name
594
+ * @returns {string|string[]|undefined}
595
+ * @throws {TypeError} if field is missing or is not a string
596
+ */
597
+ get(field) {
598
+ if (!field) {
599
+ throw new TypeError("name argument is required to req.get");
600
+ }
601
+ if (typeof field !== "string") {
602
+ throw new TypeError("name must be a string to req.get");
603
+ }
604
+ field = field.toLowerCase();
605
+ if (field === "referrer" || field === "referer") {
606
+ const res = this.headers["referrer"];
607
+ if (!res) {
608
+ return this.headers["referer"];
609
+ }
610
+ return res;
611
+ }
612
+ return this.headers[field];
613
+ }
614
+
615
+ header = this.get;
616
+
617
+ /**
618
+ * Picks the best of the given types against the Accept header.
619
+ * @param {...(string|string[])} types extensions or mime types
620
+ * @returns {string|string[]|false} the best match, false if none is acceptable, or every
621
+ * acceptable type when called with no arguments
622
+ */
623
+ accepts(...types) {
624
+ return accepts(asMessage(this)).types(.../** @type {any} */ (types));
625
+ }
626
+
627
+ /**
628
+ * The same, against Accept-Charset.
629
+ * @param {...(string|string[])} charsets
630
+ * @returns {string|string[]|false}
631
+ */
632
+ acceptsCharsets(...charsets) {
633
+ return accepts(asMessage(this)).charsets(.../** @type {any} */ (charsets));
634
+ }
635
+
636
+ /**
637
+ * The same, against Accept-Encoding.
638
+ * @param {...(string|string[])} encodings
639
+ * @returns {string|string[]|false}
640
+ */
641
+ acceptsEncodings(...encodings) {
642
+ return accepts(asMessage(this)).encodings(.../** @type {any} */ (encodings));
643
+ }
644
+
645
+ /**
646
+ * The same, against Accept-Language.
647
+ * @param {...(string|string[])} languages
648
+ * @returns {string|string[]|false}
649
+ */
650
+ acceptsLanguages(...languages) {
651
+ return accepts(asMessage(this)).languages(.../** @type {any} */ (languages));
652
+ }
653
+
654
+ /**
655
+ * @deprecated the singular spelling Express 4 carried; use acceptsEncodings
656
+ * @param {...(string|string[])} args
657
+ * @returns {string|string[]|false}
658
+ */
659
+ acceptsEncoding(...args) {
660
+ deprecated("req.acceptsEncoding", "req.acceptsEncodings");
661
+ return this.acceptsEncodings(...args);
662
+ }
663
+
664
+ /**
665
+ * @deprecated the singular spelling Express 4 carried; use acceptsCharsets
666
+ * @param {...(string|string[])} args
667
+ * @returns {string|string[]|false}
668
+ */
669
+ acceptsCharset(...args) {
670
+ deprecated("req.acceptsCharset", "req.acceptsCharsets");
671
+ return this.acceptsCharsets(...args);
672
+ }
673
+
674
+ /**
675
+ * @deprecated the singular spelling Express 4 carried; use acceptsLanguages
676
+ * @param {...(string|string[])} args
677
+ * @returns {string|string[]|false}
678
+ */
679
+ acceptsLanguage(...args) {
680
+ deprecated("req.acceptsLanguage", "req.acceptsLanguages");
681
+ return this.acceptsLanguages(...args);
682
+ }
683
+
684
+ /**
685
+ * Whether the request body's Content-Type matches. Accepts extensions ("json"), mime types
686
+ * ("application/json") and wildcards ("application/*").
687
+ *
688
+ * @param {string|string[]} types one or several, as an array or as separate arguments
689
+ * @returns {string|false|null} the matching type, false if it does not match, null if there
690
+ * is no body to have a type
691
+ */
692
+ is(types) {
693
+ if (Array.isArray(types)) {
694
+ return typeis(asMessage(this), types);
695
+ }
696
+
697
+ if (arguments.length === 1) {
698
+ return typeis(asMessage(this), [types]);
699
+ }
700
+
701
+ return typeis(asMessage(this), [...arguments]);
702
+ }
703
+
704
+ /**
705
+ * Parses the Range header against a resource of the given size.
706
+ *
707
+ * @param {number} size length of the resource being served
708
+ * @param {{combine?: boolean}} [options] combine adjacent and overlapping ranges
709
+ * @returns {Array|number|undefined} the ranges, -1 when unsatisfiable, -2 when malformed,
710
+ * or undefined when there is no Range header
711
+ */
712
+ range(size, options) {
713
+ const range = this.headers["range"];
714
+ if (!range) return;
715
+ return parseRange(size, range, options);
716
+ }
717
+
718
+ /**
719
+ * Only here so a getter does not make the property read-only. Middleware that rewrites the
720
+ * request headers wholesale assigns to it, and Express lets it.
721
+ */
722
+ set headers(headers) {
723
+ this.#cachedHeaders = headers;
724
+ }
725
+
726
+ /**
727
+ * The request headers as node presents them: lowercased names, and repeats folded the way
728
+ * node folds them. Set-Cookie stays an array, Cookie is joined with "; ", the fields listed in
729
+ * discardedDuplicates keep only the first value, and everything else is joined with ", ".
730
+ *
731
+ * Built on first read and cached, since the raw entries are what routing works from and most
732
+ * requests never ask for this at all.
733
+ *
734
+ * @returns {Record<string, any>}
735
+ */
736
+ get headers() {
737
+ // https://nodejs.org/api/http.html#messageheaders
738
+ if (this.#cachedHeaders) {
739
+ return this.#cachedHeaders;
740
+ }
741
+ // built into a local and published at the end, so a throw partway through cannot leave a
742
+ // half-filled object cached
743
+ const headers = { ...new NullObject() }; // seems to be faster
744
+ const entries = this.#rawHeadersEntries;
745
+ for (let index = 0, len = entries.length; index < len; index += 2) {
746
+ const value = entries[index + 1];
747
+ const key = entries[index].toLowerCase();
748
+ if (headers[key]) {
749
+ if (discardedDuplicates.has(key)) {
750
+ continue;
751
+ }
752
+ if (key === "cookie") {
753
+ headers[key] += "; " + value;
754
+ } else if (key === "set-cookie") {
755
+ headers[key].push(value);
756
+ } else {
757
+ headers[key] += ", " + value;
758
+ }
759
+ continue;
760
+ }
761
+ if (key === "set-cookie") {
762
+ headers[key] = [value];
763
+ } else {
764
+ headers[key] = value;
765
+ }
766
+ }
767
+ this.#cachedHeaders = headers;
768
+ return headers;
769
+ }
770
+
771
+ /**
772
+ * The same headers with every repeat kept, each name mapping to an array. node exposes this
773
+ * alongside the folded form for the callers that need to tell one header sent twice from one
774
+ * header carrying a comma.
775
+ *
776
+ * @returns {Record<string, string[]>}
777
+ */
778
+ get headersDistinct() {
779
+ if (this.#cachedDistinctHeaders) {
780
+ return this.#cachedDistinctHeaders;
781
+ }
782
+ const distinct = { ...new NullObject() };
783
+ const entries = this.#rawHeadersEntries;
784
+ for (let index = 0, len = entries.length; index < len; index += 2) {
785
+ const key = entries[index];
786
+ const value = entries[index + 1];
787
+ if (!distinct[key]) {
788
+ distinct[key] = [value];
789
+ } else {
790
+ distinct[key].push(value);
791
+ }
792
+ }
793
+ this.#cachedDistinctHeaders = distinct;
794
+ return distinct;
795
+ }
796
+
797
+ /**
798
+ * The headers as a flat list, name then value, in the order they arrived and with the case
799
+ * they arrived in. Same shape as node.
800
+ * @returns {string[]}
801
+ */
802
+ get rawHeaders() {
803
+ // a copy, since this is exactly how the headers are kept and handing the array itself out
804
+ // would let a caller rewrite what routing reads
805
+ return this.#rawHeadersEntries.slice();
806
+ }
807
+ };