fulmine.js 5.2.0 → 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.
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
 
@@ -200,13 +335,20 @@ module.exports = class Request extends Readable {
200
335
  ) {
201
336
  r._connectionClose = true;
202
337
  } else if (
203
- // content-length: 0 declares that there is nothing, which is the same as declaring
204
- // nothing: the stream ends empty either way, without the onData subscription
205
- (headerKey.length === 14 && headerKey === "content-length" && value !== "0") ||
338
+ (headerKey.length === 14 && headerKey === "content-length") ||
206
339
  (headerKey.length === 17 && headerKey === "transfer-encoding")
207
340
  ) {
208
- // noticed here so the body decision in the constructor does not build the headers object
209
- 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
+ }
210
352
  }
211
353
  };
212
354
 
@@ -216,6 +358,88 @@ module.exports = class Request extends Readable {
216
358
  */
217
359
  optimizedParams;
218
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
+
219
443
  /**
220
444
  * The continuation of the chain currently running, which express also hands to a handler
221
445
  * through the request. Declared rather than left to appear on assignment: runRoute sets it
@@ -252,11 +476,13 @@ module.exports = class Request extends Readable {
252
476
  * literal registration, a holder of its own for a parameterised one
253
477
  */
254
478
  constructor(req, res, app, preset, skipHolder) {
255
- // the same object every time: Readable reads these options and never writes to them
256
- super(READABLE_OPTIONS);
479
+ // nothing: the stream is built on the first touch, see LazyReadable
480
+ super();
257
481
  this._res = res;
258
482
  this._req = req;
259
- 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;
260
486
  if (skipHolder !== undefined && skipHolder.skipHeaders) {
261
487
  // The chain behind this registration provably never reads a header, so instead of
262
488
  // copying them all out of uWS the constructor asks for the four that steer the
@@ -265,6 +491,9 @@ module.exports = class Request extends Readable {
265
491
  // the parsers and the stream want the whole picture, so it takes the full copy.
266
492
  const length = req.getHeader("content-length");
267
493
  const transferEncoding = req.getHeader("transfer-encoding");
494
+ if (length !== "" || transferEncoding !== "") {
495
+ this._hasBodyHeaders = true;
496
+ }
268
497
  if ((length !== "" && length !== "0") || transferEncoding !== "") {
269
498
  currentRequest = this;
270
499
  this._req.forEach(Request.#collectHeader);
@@ -318,8 +547,11 @@ module.exports = class Request extends Readable {
318
547
  this._rawQuery = "";
319
548
  this.urlQuery = "";
320
549
  } else {
321
- this._rawQuery = req.getQuery() ?? "";
322
- 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;
323
555
  }
324
556
  if (preset) {
325
557
  // the registration's constants: two native crossings and their strings not asked for
@@ -347,9 +579,6 @@ module.exports = class Request extends Readable {
347
579
  this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
348
580
  this._opPath = this.path;
349
581
  this._originalPath = this.path;
350
- if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
351
- this._opPath = this._opPath.slice(0, -1);
352
- }
353
582
  this.method = req.getCaseSensitiveMethod().toUpperCase();
354
583
  this._isOptions = this.method === "OPTIONS";
355
584
  this._isHead = this.method === "HEAD";
@@ -370,10 +599,13 @@ module.exports = class Request extends Readable {
370
599
  // null for the same reason as the two above: a request that never enters a mount never
371
600
  // needs either array, and the push sites materialize them
372
601
  this._stack = null;
373
- // number of entries in _stack that aren't the empty path. while this is 0 the whole
374
- // stack joins to "", so getFullMountpath can skip the join entirely
375
- 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;
376
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;
377
609
  this.receivedData = false;
378
610
  // reading ip is very slow in UWS, so its better to not do it unless truly needed
379
611
  if (this.app.needsIpAfterResponse || this.key < 100) {
@@ -483,8 +715,8 @@ module.exports = class Request extends Readable {
483
715
  if (this._baseUrlOverride !== undefined) {
484
716
  return this._baseUrlOverride;
485
717
  }
486
- const match = this._originalPath.match(this.app.getFullMountpath(this));
487
- 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);
488
720
  }
489
721
 
490
722
  /**
@@ -641,13 +873,13 @@ module.exports = class Request extends Readable {
641
873
  ? this._originalPath
642
874
  : this._originalPath.slice(0, this._originalPath.length - oldPath.length);
643
875
  this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
644
- 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;
645
878
  this.#cachedQuery = null;
646
879
  this._originalPath = prefix + newPath;
647
880
  this.path = newPath;
648
881
  this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
649
- this._opPath =
650
- this.endsWithSlash && newPath !== "/" && !this.app.get("strict routing") ? newPath.slice(0, -1) : newPath;
882
+ this._opPath = newPath;
651
883
  this._lastUrl = newUrl;
652
884
  }
653
885
 
@@ -741,22 +973,34 @@ module.exports = class Request extends Readable {
741
973
  }
742
974
  this.rawIp = this._res.getRemoteAddress();
743
975
  }
976
+ // read once: the branch above settled it, and every use below wants the bytes
977
+ const rawIp = /** @type {ArrayBuffer} */ (this.rawIp);
744
978
  /** @type {string|undefined} */
745
979
  let ip;
746
- if (this.rawIp.byteLength === 4) {
980
+ if (rawIp.byteLength === 4) {
747
981
  // ipv4
748
- ip = new Uint8Array(this.rawIp).join(".");
982
+ ip = new Uint8Array(rawIp).join(".");
749
983
  if (mapsIPv4Peer(this.app)) {
750
984
  ip = "::ffff:" + ip;
751
985
  }
752
- } else if (this.rawIp.byteLength === 16) {
753
- // ipv6
754
- const dv = new DataView(this.rawIp);
755
- const groups = new Array(8);
756
- for (let i = 0; i < 8; i++) {
757
- 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);
758
1003
  }
759
- ip = formatIPv6(groups);
760
1004
  } else {
761
1005
  ip = undefined; // unix sockets dont have ip
762
1006
  }
package/src/response.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
@@ -188,8 +190,33 @@ module.exports = class Response extends Writable {
188
190
  this._corkNeeded = false;
189
191
  // shared methods, not arrows: two closures and a once() wrapper here were four
190
192
  // allocations per request. EventEmitter calls listeners with this = the emitter.
191
- this.on("error", this._onAbortError);
192
- this.on("close", this._onCloseCleanup);
193
+ //
194
+ // Written into the map rather than through on(). A stream arrives with its _events already
195
+ // shaped, "close" and "error" among the keys and every value undefined, so filling two of
196
+ // them is the same hidden class on() would have produced and none of its work: the argument
197
+ // check, the newListener emission, the is-there-one-already branch and the max listener
198
+ // count. Two calls per response, and the profile put them at 6% of a hello-world.
199
+ //
200
+ // The condition is the whole safety of it: a fresh response has no listeners, so both slots
201
+ // are free, and anything else falls back to on(). Adding one later goes through on() as
202
+ // usual and finds what this wrote, because this wrote what on() writes.
203
+ // cast because _events and _eventsCount are EventEmitter's own bookkeeping and carry no
204
+ // type: they are what on() writes, and this writes the same two entries
205
+ const self = /** @type {any} */ (this);
206
+ const events = self._events;
207
+ if (
208
+ self._eventsCount === 0 &&
209
+ events !== undefined &&
210
+ events.error === undefined &&
211
+ events.close === undefined
212
+ ) {
213
+ events.error = this._onAbortError;
214
+ events.close = this._onCloseCleanup;
215
+ self._eventsCount = 2;
216
+ } else {
217
+ this.on("error", this._onAbortError);
218
+ this.on("close", this._onCloseCleanup);
219
+ }
193
220
  }
194
221
 
195
222
  /** @param {Error} err */
@@ -703,7 +730,10 @@ module.exports = class Response extends Writable {
703
730
  // Express's completion handler, exactly: a callback hears everything and the response is
704
731
  // left alone, so it can still answer 200 after a 404 error. Without one, a directory
705
732
  // falls through as a plain next(), and aborts and write errors go nowhere.
706
- const next = this.req.next;
733
+ // the router's next and not the route's: express reports a file it could not serve past
734
+ // the rest of the route, so a four argument handler written inside the route never sees
735
+ // it. _leaveRoute is that; req.next is kept for everything else, see Walk#stepOutOfRoute
736
+ const next = this.req._leaveRoute ?? this.req.next;
707
737
  const done = /** @type {(err?: any) => void} */ (
708
738
  (err) => {
709
739
  if (callback) return callback(err);
@@ -857,9 +887,6 @@ module.exports = class Response extends Writable {
857
887
  if (options.etag && !this.headers["etag"]) {
858
888
  this.headers["etag"] = statTag(stat, true);
859
889
  }
860
- if (!options.etag) {
861
- this.req.noEtag = true;
862
- }
863
890
 
864
891
  // announced before the conditional checks, because those can return early and the header
865
892
  // still belongs on the response. send does it in the same order, so a 412 or a 416 still
@@ -928,6 +955,14 @@ module.exports = class Response extends Writable {
928
955
  }
929
956
  }
930
957
 
958
+ // Turning the etag off means this file goes out without one, and only this file: the
959
+ // exits above hand an error to the application, and the body its handler sends computes
960
+ // its own etag, as it does on express. Suppressing it before those exits made a 416 or a
961
+ // 412 answer without one.
962
+ if (!options.etag) {
963
+ this.req.noEtag = true;
964
+ }
965
+
931
966
  // anything but the whole file, whether from a Range header or the start/end options,
932
967
  // has to go through the read stream with explicit bounds
933
968
  const partial = offset > 0 || len < stat.size;
@@ -1226,7 +1261,11 @@ module.exports = class Response extends Writable {
1226
1261
  const done =
1227
1262
  callback ||
1228
1263
  ((err, str) => {
1229
- if (err) return this.req.next(err);
1264
+ // the router's next and not the route's, the same as sendFile: express reports a
1265
+ // view it could not render to req.next, which the router owns, so the rest of the
1266
+ // route is skipped and a four argument handler written inside it never sees the
1267
+ // error. A callback, when there is one, hears everything instead
1268
+ if (err) return (this.req._leaveRoute ?? this.req.next)(err);
1230
1269
  this.send(str);
1231
1270
  });
1232
1271
 
@@ -1323,6 +1362,12 @@ module.exports = class Response extends Writable {
1323
1362
 
1324
1363
  this.vary("Accept");
1325
1364
 
1365
+ // req.next, not the router next sendFile and render use. Express's is the router's here
1366
+ // too, so inside a route with a four argument handler of its own this hands the error to
1367
+ // that handler where express would have left the route. Passing the router next instead
1368
+ // fails express's own res.format test, which asserts the handler is given the very same
1369
+ // function the surrounding middleware received: outside a route the two are equivalent but
1370
+ // not identical. The whole thing is the open req.next question at Walk's constructor
1326
1371
  if (key) {
1327
1372
  this.set("Content-Type", normalizeType(key).value);
1328
1373
  object[key](this.req, this, this.req.next);