fulmine.js 5.1.9 → 5.3.0

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,110 @@
1
+ /*
2
+ Copyright 2026 Nigro Simone
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The option bags the file-serving and body-parsing paths take, written once and referenced from
18
+ // the JSDoc of the functions that read them. They live in a declaration file rather than as
19
+ // @typedef blocks in the sources because those sources export classes, and a typedef hanging off
20
+ // a `module.exports = class` module makes TypeScript see two unrelated copies of the class.
21
+
22
+ /** What res.sendFile, express.static and res.download all read. The names and defaults are send's. */
23
+ export interface SendFileOptions {
24
+ /** The directory a relative path resolves against, and the boundary nothing may climb out of. */
25
+ root?: string;
26
+ /** Cache-Control's max-age, in milliseconds or as a duration such as "1d". */
27
+ maxAge?: number | string;
28
+ /** Adds Cache-Control: immutable, which is only meaningful next to a long maxAge. */
29
+ immutable?: boolean;
30
+ /** Whether Last-Modified is sent, from the file's mtime. */
31
+ lastModified?: boolean;
32
+ /** Whether an ETag is sent. */
33
+ etag?: boolean;
34
+ /** Whether a Range request is honoured. */
35
+ acceptRanges?: boolean;
36
+ /** Whether Cache-Control is sent at all. */
37
+ cacheControl?: boolean;
38
+ /**
39
+ * What to do with a path holding a dotfile. "ignore_files" is one more than send offers:
40
+ * it hides a dotfile that is the last segment while letting a dotted directory through.
41
+ */
42
+ dotfiles?: "allow" | "deny" | "ignore" | "ignore_files";
43
+ /** Extra headers for the response. */
44
+ headers?: Record<string, string>;
45
+ /** Called before the file goes out, to set headers from the path or its stat. */
46
+ setHeaders?: (res: any, path: string, stat: any) => void;
47
+ /** First byte of the window to send. */
48
+ start?: number;
49
+ /** Last byte of the window to send. */
50
+ end?: number;
51
+ /** The path is already encoded, so leave it alone. */
52
+ skipEncodePath?: boolean;
53
+ /** Internal: locals carried through to a view render. */
54
+ _locals?: Record<string, any>;
55
+ /** Internal: the stat the caller already took, so it is not taken twice. */
56
+ _stat?: any;
57
+ /** Internal: the caller computed the ETag itself. */
58
+ _ownEtag?: boolean;
59
+ }
60
+
61
+ /** What express.static reads on top of everything res.sendFile takes. */
62
+ export interface StaticOptions extends SendFileOptions {
63
+ /** The file served for a directory, or false to serve none. */
64
+ index?: string | false;
65
+ /** Whether a directory without a trailing slash is redirected to one. */
66
+ redirect?: boolean;
67
+ /** Whether a request this middleware cannot serve moves on instead of being answered. */
68
+ fallthrough?: boolean;
69
+ /** Extensions tried when the path names no file, or false to try none. */
70
+ extensions?: string[] | false;
71
+ }
72
+
73
+ /** A body parser's options once its factory has filled in every default it needs. */
74
+ export interface SettledBodyParserOptions extends BodyParserOptions {
75
+ limit: number;
76
+ inflate: boolean;
77
+ /** the string form is turned into a one-element list by the factory */
78
+ type: string[] | ((req: any) => boolean);
79
+ defaultCharset: string;
80
+ }
81
+
82
+ /** What the body parsers take. The four share these; each names below the ones it alone reads. */
83
+ export interface BodyParserOptions {
84
+ /** The largest body to accept, as bytes or as "100kb". */
85
+ limit?: number | string;
86
+ /** Which content types this parser claims. */
87
+ type?: string | string[] | ((req: any) => boolean);
88
+ /** Runs on the raw bytes before parsing, which is where a signature check belongs. */
89
+ verify?: false | ((req: any, res: any, buf: Buffer, encoding: string) => void);
90
+ /** Whether a compressed body is decompressed rather than refused. */
91
+ inflate?: boolean;
92
+ /** The charset assumed when the request names none. */
93
+ defaultCharset?: string;
94
+ /** json only: refuse a body that is not an object or an array. */
95
+ strict?: boolean;
96
+ /** json only, passed to JSON.parse. */
97
+ reviver?: (key: string, value: any) => any;
98
+ /** urlencoded only: parse with qs rather than the plain parser. */
99
+ extended?: boolean;
100
+ /** urlencoded only: how many parameters to accept. */
101
+ parameterLimit?: number;
102
+ /** urlencoded only, extended only: how deep a nested object may go. */
103
+ depth?: number;
104
+ /** urlencoded only, passed to qs. */
105
+ charsetSentinel?: boolean;
106
+ /** urlencoded only, passed to qs. */
107
+ interpretNumericEntities?: boolean;
108
+ /** Internal: the single type this parser claims, which lets the prologue compare strings. */
109
+ simpleType?: string;
110
+ }
@@ -1,8 +1,25 @@
1
+ /*
2
+ Copyright 2026 Nigro Simone
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
1
17
  "use strict";
2
18
 
3
19
  /*
4
- The parser from fast-querystring 1.1.x (MIT, Copyright (c) Yagiz Nizipli,
5
- https://github.com/anonrig/fast-querystring), vendored for one change: the result is a bare
20
+ The parser from fast-querystring 1.1.x (https://github.com/anonrig/fast-querystring), Copyright
21
+ (c) 2022 Yagiz Nizipli, MIT, whose permission notice is reproduced in full in NOTICE at the root
22
+ of this package. Vendored for one change: the result is a bare
6
23
  Object.create(null) instead of the library's Empty-constructor trick. The trick is faster to
7
24
  construct but node inspects it as "Empty <[Object: null prototype] {}>", where Express shows
8
25
  "[Object: null prototype]", and matching that used to cost an Object.assign copy of every parse
package/src/request.js CHANGED
@@ -2,6 +2,8 @@
2
2
  Copyright 2024 dimden.dev
3
3
  Copyright 2026 Nigro Simone
4
4
 
5
+ This file is derived from Ultimate Express and has been modified.
6
+
5
7
  Licensed under the Apache License, Version 2.0 (the "License");
6
8
  you may not use this file except in compliance with the License.
7
9
  You may obtain a copy of the License at
@@ -84,6 +86,31 @@ function formatIPv6(groups) {
84
86
  return out;
85
87
  }
86
88
 
89
+ /**
90
+ * Whether these sixteen bytes are an IPv4-mapped address, ::ffff:0:0/96: ten zero bytes and then
91
+ * 0xffff. Ten comparisons rather than a loop, because this runs on every address that is read and
92
+ * the first mismatch answers immediately for a real IPv6 peer.
93
+ *
94
+ * @param {Uint8Array} bytes exactly sixteen of them
95
+ * @returns {boolean}
96
+ */
97
+ function isMappedIPv4(bytes) {
98
+ return (
99
+ bytes[10] === 0xff &&
100
+ bytes[11] === 0xff &&
101
+ bytes[0] === 0 &&
102
+ bytes[1] === 0 &&
103
+ bytes[2] === 0 &&
104
+ bytes[3] === 0 &&
105
+ bytes[4] === 0 &&
106
+ bytes[5] === 0 &&
107
+ bytes[6] === 0 &&
108
+ bytes[7] === 0 &&
109
+ bytes[8] === 0 &&
110
+ bytes[9] === 0
111
+ );
112
+ }
113
+
87
114
  /**
88
115
  * Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps
89
116
  * it whenever the listener is dual stack, which is every listen() not given an IPv4 address to
@@ -129,7 +156,115 @@ const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
129
156
  // of once per request.
130
157
  let currentRequest = null;
131
158
 
132
- module.exports = class Request extends Readable {
159
+ /**
160
+ * A Readable that has not been built yet.
161
+ *
162
+ * Every request pays for the stream and almost none of them use it: a GET carries no body, and the
163
+ * bodies that do arrive are collected by µWS and handed to the parsers without the stream being
164
+ * touched. Measured on this machine, running Readable's constructor costs about 90ns of the 900ns
165
+ * a hello-world request costs in total, which is a tenth of it for a facility nobody asked for.
166
+ *
167
+ * So the chain says Readable and the constructor does not run. `Request extends LazyReadable`, and
168
+ * LazyReadable's prototype is Readable's, which keeps `req instanceof Readable` true and every
169
+ * Readable method reachable; what is missing is `_readableState`, and that is built on the first
170
+ * touch. A derived class cannot skip its super() call, but a base class with nothing in it costs
171
+ * nothing to call.
172
+ *
173
+ * The wrapping below is generated rather than written out, and deliberately: every own member of
174
+ * Readable's prototype gets a version that materialises first, so there is no list to keep in step
175
+ * and no door left unguarded. Missing one would not be a slow path, it would be a TypeError on
176
+ * `undefined._readableState` in whatever corner of a stream nobody tested.
177
+ */
178
+ class LazyReadableBase {}
179
+ Object.setPrototypeOf(LazyReadableBase.prototype, Readable.prototype);
180
+ Object.setPrototypeOf(LazyReadableBase, Readable);
181
+
182
+ // what the chain says at runtime, said again for the type checker, which cannot see a prototype
183
+ // being reassigned: everything a Readable offers is reachable from a Request, and is a Readable's
184
+ const LazyReadable = /** @type {typeof Readable} */ (/** @type {unknown} */ (LazyReadableBase));
185
+
186
+ /**
187
+ * Builds the stream this object has been pretending to be. Idempotent: everything that can be
188
+ * reached from outside goes through it, so it is called far more often than it does anything.
189
+ *
190
+ * EventEmitter's init keeps an _events that is already there, so listeners added before this
191
+ * survive it.
192
+ *
193
+ * @param {any} stream
194
+ */
195
+ function materialise(stream) {
196
+ if (stream._readableState === undefined) {
197
+ Readable.call(stream, READABLE_OPTIONS);
198
+ }
199
+ }
200
+
201
+ for (const member of [
202
+ ...Object.getOwnPropertyNames(Readable.prototype),
203
+ ...Object.getOwnPropertySymbols(Readable.prototype)
204
+ ]) {
205
+ // the constructor is not a door, and `readable` is handled below because a request writes it
206
+ // and writing it must not build the very thing this is avoiding
207
+ if (member === "constructor" || member === "readable") {
208
+ continue;
209
+ }
210
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Readable.prototype, member));
211
+ if (typeof descriptor.value === "function") {
212
+ const inner = descriptor.value;
213
+ Object.defineProperty(LazyReadableBase.prototype, member, {
214
+ ...descriptor,
215
+ /** @this {any} @param {...any} args */
216
+ value: function (...args) {
217
+ materialise(this);
218
+ return inner.apply(this, args);
219
+ }
220
+ });
221
+ } else if (descriptor.get || descriptor.set) {
222
+ const innerGet = descriptor.get;
223
+ const innerSet = descriptor.set;
224
+ Object.defineProperty(LazyReadableBase.prototype, member, {
225
+ ...descriptor,
226
+ get: innerGet
227
+ ? /** @this {any} */ function () {
228
+ materialise(this);
229
+ return innerGet.call(this);
230
+ }
231
+ : undefined,
232
+ set: innerSet
233
+ ? /** @this {any} @param {any} value */ function (value) {
234
+ materialise(this);
235
+ innerSet.call(this, value);
236
+ }
237
+ : undefined
238
+ });
239
+ }
240
+ }
241
+
242
+ const nodeReadable = /** @type {PropertyDescriptor} */ (
243
+ Object.getOwnPropertyDescriptor(Readable.prototype, "readable")
244
+ );
245
+
246
+ // `readable` on its own: a request sets it while it is being built, and node's setter is a no-op
247
+ // without the state anyway, so the flag is kept as a plain field until there is a stream to ask
248
+ Object.defineProperty(LazyReadableBase.prototype, "readable", {
249
+ configurable: true,
250
+ enumerable: false,
251
+ /** @this {any} */
252
+ get: function () {
253
+ return this._readableState === undefined
254
+ ? this._readableFlag === true
255
+ : /** @type {any} */ (nodeReadable.get).call(this);
256
+ },
257
+ /** @this {any} @param {any} value */
258
+ set: function (value) {
259
+ if (this._readableState === undefined) {
260
+ this._readableFlag = !!value;
261
+ return;
262
+ }
263
+ /** @type {any} */ (nodeReadable.set).call(this, value);
264
+ }
265
+ });
266
+
267
+ module.exports = class Request extends LazyReadable {
133
268
  /** @type {Record<string, any>|null} */
134
269
  #cachedQuery = null;
135
270
 
@@ -139,26 +274,53 @@ module.exports = class Request extends Readable {
139
274
  /** @type {Record<string, string[]>|null} */
140
275
  #cachedDistinctHeaders = null;
141
276
 
142
- // Flat, name then value: an array of pairs meant one array allocated per header on every
143
- // request, and a request carries eight or ten of them. Everything that reads this walks it two
144
- // at a time. The names are lowercase by contract: uWS lowers them on the wire and the node
145
- // shim lowers them in its forEach, so readers compare without lowering again.
277
+ /**
278
+ * Every header, flat: name then value, name then value.
279
+ *
280
+ * An array of pairs meant one array allocated per header on every request, and a request
281
+ * carries eight or ten of them, so everything that reads this walks it two at a time. The
282
+ * names are lowercase by contract: uWS lowers them on the wire and the node shim lowers
283
+ * them in its forEach, so readers compare without lowering again.
284
+ *
285
+ * @type {string[]}
286
+ */
146
287
  #rawHeadersEntries = [];
147
288
 
148
289
  /** @type {string|undefined|null} */
149
290
  #cachedParsedIp = null;
150
291
 
292
+ /** Whether backpressure has asked uWS to stop delivering the body for now. */
151
293
  #paused = false;
152
294
 
153
- // a bodyless request whose empty end has not been delivered yet, see the constructor
295
+ /** A bodyless request whose empty end has not been delivered yet, see the constructor. */
154
296
  #emptyBody = false;
155
297
 
298
+ /**
299
+ * What a body parser left behind, and undefined until one claims the request.
300
+ * @type {any}
301
+ */
156
302
  body;
157
303
 
304
+ /**
305
+ * The response this request arrived with, linked so either reaches the other.
306
+ *
307
+ * Typed loosely on purpose: it is linked right after construction rather than in the
308
+ * constructor, and the honest `Response|undefined` would put a check in front of every
309
+ * use of a field that is never observed unset.
310
+ *
311
+ * @type {any}
312
+ */
158
313
  res;
159
314
 
160
- // one function for every request, fed through currentRequest: an arrow in the constructor
161
- // captured `this`, which cost a context and a function allocation per request
315
+ /**
316
+ * Copies one header out of uWS and notices the two things the constructor decides by.
317
+ *
318
+ * One function for every request, fed through currentRequest: an arrow in the constructor
319
+ * captured `this`, which cost a context and a function allocation per request.
320
+ *
321
+ * @param {string} headerKey lowercase, as uWS hands it over
322
+ * @param {string} value
323
+ */
162
324
  static #collectHeader = (headerKey, value) => {
163
325
  const r = currentRequest;
164
326
  r.#rawHeadersEntries.push(headerKey, value);
@@ -173,20 +335,130 @@ module.exports = class Request extends Readable {
173
335
  ) {
174
336
  r._connectionClose = true;
175
337
  } else if (
176
- // content-length: 0 declares that there is nothing, which is the same as declaring
177
- // nothing: the stream ends empty either way, without the onData subscription
178
- (headerKey.length === 14 && headerKey === "content-length" && value !== "0") ||
338
+ (headerKey.length === 14 && headerKey === "content-length") ||
179
339
  (headerKey.length === 17 && headerKey === "transfer-encoding")
180
340
  ) {
181
- // noticed here so the body decision in the constructor does not build the headers object
182
- r._declaresBody = true;
341
+ // saying anything about framing at all, "0" included. A parser that can see a
342
+ // content-length answers about the body it describes, even an empty one: a zero length
343
+ // with a charset nobody can decode is a 415 in express and here, so a chain may only
344
+ // step over a parser when the request said nothing about a body whatsoever
345
+ r._hasBodyHeaders = true;
346
+ // content-length: 0 declares that there is nothing, which is the same as declaring
347
+ // nothing: the stream ends empty either way, without the onData subscription
348
+ if (value !== "0" || headerKey.length === 17) {
349
+ // noticed here so the body decision in the constructor does not build the headers object
350
+ r._declaresBody = true;
351
+ }
183
352
  }
184
353
  };
185
354
 
355
+ /**
356
+ * The parameters a native uWS route matched, by name, or undefined off that path.
357
+ * @type {Record<string, string>|undefined}
358
+ */
186
359
  optimizedParams;
187
360
 
361
+ /**
362
+ * Whether a body parser has already read this request, so a second one leaves it alone.
363
+ * @type {boolean|undefined}
364
+ */
365
+ bodyRead;
366
+
367
+ /**
368
+ * The route currently running, which express hands to a handler through the request.
369
+ * @type {any}
370
+ */
371
+ route;
372
+
373
+ /**
374
+ * Which hop the error being carried came from, so an error handler declared before it does
375
+ * not catch what happened after it.
376
+ * @type {number|undefined}
377
+ */
378
+ _errorKey;
379
+
380
+ /**
381
+ * Which app.route() the failing route belonged to, when it belonged to one. Express builds one
382
+ * route out of everything hung off an app.route(), so an error handler written on it catches
383
+ * what its siblings raised, and nothing else does.
384
+ * @type {number|undefined}
385
+ */
386
+ _errorGroup;
387
+
388
+ /**
389
+ * How much of _originalPath the mounts entered so far have taken. Kept as a count rather than
390
+ * worked out from the mount patterns, because what a mount took is what it matched, and a
391
+ * pattern rebuilt from the whole stack does not always match the same thing.
392
+ * @type {number}
393
+ */
394
+ _consumed = 0;
395
+
396
+ /**
397
+ * next() as the router means it: the rest of the route is skipped. res.sendFile reports its
398
+ * failures here, because express reports them to the router and not to the route.
399
+ * @type {((err?: any) => void)|undefined}
400
+ */
401
+ _leaveRoute;
402
+
403
+ /**
404
+ * What `readable` answers while there is no stream to ask, see LazyReadable. Declared so the
405
+ * class has one shape whether or not anything ever streams.
406
+ * @type {boolean}
407
+ */
408
+ _readableFlag = true;
409
+
410
+ /**
411
+ * The peer address as uWS hands it over, sixteen bytes or four.
412
+ *
413
+ * Declared although the constructor only sometimes fills it in: a property that appears on
414
+ * some requests and not others gives the class more than one shape, and every read of every
415
+ * other field pays for that.
416
+ *
417
+ * @type {ArrayBuffer|undefined}
418
+ */
419
+ rawIp;
420
+
421
+ /**
422
+ * Whether the request declared a body, content-length or transfer-encoding, spotted during
423
+ * the header copy. Declared for the same reason as rawIp.
424
+ * @type {boolean|undefined}
425
+ */
426
+ _declaresBody;
427
+
428
+ /**
429
+ * Whether the request said anything at all about framing, a content-length of "0" included.
430
+ * Wider than _declaresBody on purpose: a parser that can see a content-length answers about
431
+ * the body it describes even when that body is empty, so this is what decides whether a chain
432
+ * may step over one. Declared for the same reason as rawIp.
433
+ * @type {boolean|undefined}
434
+ */
435
+ _hasBodyHeaders;
436
+
437
+ /**
438
+ * Whether the client asked for the connection to be closed. Declared for the same reason.
439
+ * @type {boolean|undefined}
440
+ */
441
+ _connectionClose;
442
+
443
+ /**
444
+ * The continuation of the chain currently running, which express also hands to a handler
445
+ * through the request. Declared rather than left to appear on assignment: runRoute sets it
446
+ * on every request, and an undeclared property is a shape change on each one.
447
+ *
448
+ * @type {any}
449
+ */
450
+ next;
451
+
452
+ /**
453
+ * What the chain threw or passed to next(err), waiting for an error handler.
454
+ * @type {any}
455
+ */
188
456
  _error;
189
457
 
458
+ /**
459
+ * Set by the paths that must not earn an ETag, res.sendFile's stream among them.
460
+ * @type {boolean|undefined}
461
+ */
190
462
  noEtag;
191
463
 
192
464
  /**
@@ -204,11 +476,13 @@ module.exports = class Request extends Readable {
204
476
  * literal registration, a holder of its own for a parameterised one
205
477
  */
206
478
  constructor(req, res, app, preset, skipHolder) {
207
- // the same object every time: Readable reads these options and never writes to them
208
- super(READABLE_OPTIONS);
479
+ // nothing: the stream is built on the first touch, see LazyReadable
480
+ super();
209
481
  this._res = res;
210
482
  this._req = req;
211
- this.readable = true;
483
+ // the plain field behind the `readable` accessor, written rather than set so a request that
484
+ // never streams never builds a stream
485
+ this._readableFlag = true;
212
486
  if (skipHolder !== undefined && skipHolder.skipHeaders) {
213
487
  // The chain behind this registration provably never reads a header, so instead of
214
488
  // copying them all out of uWS the constructor asks for the four that steer the
@@ -217,6 +491,9 @@ module.exports = class Request extends Readable {
217
491
  // the parsers and the stream want the whole picture, so it takes the full copy.
218
492
  const length = req.getHeader("content-length");
219
493
  const transferEncoding = req.getHeader("transfer-encoding");
494
+ if (length !== "" || transferEncoding !== "") {
495
+ this._hasBodyHeaders = true;
496
+ }
220
497
  if ((length !== "" && length !== "0") || transferEncoding !== "") {
221
498
  currentRequest = this;
222
499
  this._req.forEach(Request.#collectHeader);
@@ -270,8 +547,11 @@ module.exports = class Request extends Readable {
270
547
  this._rawQuery = "";
271
548
  this.urlQuery = "";
272
549
  } else {
273
- this._rawQuery = req.getQuery() ?? "";
274
- this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
550
+ // getQuery tells "/a" from "/a?": no query string at all reads undefined, an empty
551
+ // one reads "". Express keeps that lone "?" in req.url, so the two are kept apart
552
+ const rawQuery = req.getQuery();
553
+ this._rawQuery = rawQuery ?? "";
554
+ this.urlQuery = rawQuery === undefined ? "" : "?" + rawQuery;
275
555
  }
276
556
  if (preset) {
277
557
  // the registration's constants: two native crossings and their strings not asked for
@@ -299,9 +579,6 @@ module.exports = class Request extends Readable {
299
579
  this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
300
580
  this._opPath = this.path;
301
581
  this._originalPath = this.path;
302
- if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
303
- this._opPath = this._opPath.slice(0, -1);
304
- }
305
582
  this.method = req.getCaseSensitiveMethod().toUpperCase();
306
583
  this._isOptions = this.method === "OPTIONS";
307
584
  this._isHead = this.method === "HEAD";
@@ -322,10 +599,13 @@ module.exports = class Request extends Readable {
322
599
  // null for the same reason as the two above: a request that never enters a mount never
323
600
  // needs either array, and the push sites materialize them
324
601
  this._stack = null;
325
- // number of entries in _stack that aren't the empty path. while this is 0 the whole
326
- // stack joins to "", so getFullMountpath can skip the join entirely
327
- this._stackMounted = 0;
602
+ // how many characters of _originalPath the mounts entered so far have taken, which is
603
+ // where baseUrl ends and the path below them begins
604
+ this._consumed = 0;
328
605
  this._paramStack = null;
606
+ // route and application in pairs, one pair per mounted application entered from another
607
+ // application, so handing back puts the one that was current back, see rememberApp
608
+ this._appStack = undefined;
329
609
  this.receivedData = false;
330
610
  // reading ip is very slow in UWS, so its better to not do it unless truly needed
331
611
  if (this.app.needsIpAfterResponse || this.key < 100) {
@@ -435,8 +715,8 @@ module.exports = class Request extends Readable {
435
715
  if (this._baseUrlOverride !== undefined) {
436
716
  return this._baseUrlOverride;
437
717
  }
438
- const match = this._originalPath.match(this.app.getFullMountpath(this));
439
- return match ? match[0] : "";
718
+ // what the mounts took, which is where the path they left off begins
719
+ return this._consumed === 0 ? "" : this._originalPath.slice(0, this._consumed);
440
720
  }
441
721
 
442
722
  /**
@@ -593,13 +873,13 @@ module.exports = class Request extends Readable {
593
873
  ? this._originalPath
594
874
  : this._originalPath.slice(0, this._originalPath.length - oldPath.length);
595
875
  this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
596
- this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
876
+ // a rewrite to "/a?" keeps its "?", as one arriving that way does
877
+ this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
597
878
  this.#cachedQuery = null;
598
879
  this._originalPath = prefix + newPath;
599
880
  this.path = newPath;
600
881
  this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
601
- this._opPath =
602
- this.endsWithSlash && newPath !== "/" && !this.app.get("strict routing") ? newPath.slice(0, -1) : newPath;
882
+ this._opPath = newPath;
603
883
  this._lastUrl = newUrl;
604
884
  }
605
885
 
@@ -693,22 +973,34 @@ module.exports = class Request extends Readable {
693
973
  }
694
974
  this.rawIp = this._res.getRemoteAddress();
695
975
  }
976
+ // read once: the branch above settled it, and every use below wants the bytes
977
+ const rawIp = /** @type {ArrayBuffer} */ (this.rawIp);
696
978
  /** @type {string|undefined} */
697
979
  let ip;
698
- if (this.rawIp.byteLength === 4) {
980
+ if (rawIp.byteLength === 4) {
699
981
  // ipv4
700
- ip = new Uint8Array(this.rawIp).join(".");
982
+ ip = new Uint8Array(rawIp).join(".");
701
983
  if (mapsIPv4Peer(this.app)) {
702
984
  ip = "::ffff:" + ip;
703
985
  }
704
- } else if (this.rawIp.byteLength === 16) {
705
- // ipv6
706
- const dv = new DataView(this.rawIp);
707
- const groups = new Array(8);
708
- for (let i = 0; i < 8; i++) {
709
- groups[i] = dv.getUint16(i * 2);
986
+ } else if (rawIp.byteLength === 16) {
987
+ const bytes = new Uint8Array(rawIp);
988
+ if (isMappedIPv4(bytes)) {
989
+ // ::ffff:a.b.c.d, which is what a dual stack listener hands over for every IPv4
990
+ // peer, so it is what nearly every request here is. The general path below reaches
991
+ // the same string through a DataView, an array of eight groups and a scan for the
992
+ // longest run of zeros, and measured 157ns more per request for it. Anything that
993
+ // reads req.ip pays that once, and morgan reads it on every line it writes.
994
+ ip = "::ffff:" + bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
995
+ } else {
996
+ // ipv6
997
+ const dv = new DataView(rawIp);
998
+ const groups = new Array(8);
999
+ for (let i = 0; i < 8; i++) {
1000
+ groups[i] = dv.getUint16(i * 2);
1001
+ }
1002
+ ip = formatIPv6(groups);
710
1003
  }
711
- ip = formatIPv6(groups);
712
1004
  } else {
713
1005
  ip = undefined; // unix sockets dont have ip
714
1006
  }
@@ -749,6 +1041,33 @@ module.exports = class Request extends Readable {
749
1041
  return this.connection;
750
1042
  }
751
1043
 
1044
+ /**
1045
+ * Cuts this request loose from the µWS response it arrived on, keeping the two things only
1046
+ * that response could answer.
1047
+ *
1048
+ * A websocket upgrade hands the request to the socket, which outlives the response by the
1049
+ * whole life of the connection. Reading the peer address through the freed response is not
1050
+ * an error but a use after free, so the values are taken while it is still alive and an
1051
+ * inert stand-in answers anything that asks later.
1052
+ */
1053
+ _detachFromResponse() {
1054
+ const uwsRes = this._res;
1055
+ if (!this.rawIp) {
1056
+ this.rawIp = uwsRes.getRemoteAddress();
1057
+ }
1058
+ const remotePort = uwsRes.getRemotePort();
1059
+ const rawIp = this.rawIp;
1060
+ this._res = {
1061
+ getRemoteAddress: () => rawIp,
1062
+ getRemotePort: () => remotePort,
1063
+ // a body cannot arrive on an upgraded socket, and a stray reader must not reach µWS
1064
+ onData() {},
1065
+ pause() {},
1066
+ resume() {},
1067
+ close() {}
1068
+ };
1069
+ }
1070
+
752
1071
  /**
753
1072
  * Whether the client's cached copy is still good, from If-None-Match and If-Modified-Since
754
1073
  * against the response headers set so far. Only GET and HEAD can be fresh.