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/NOTICE +29 -2
- package/README.md +90 -61
- package/package.json +6 -2
- package/src/application.js +67 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +67 -6
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +16 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +275 -31
- package/src/response.js +52 -7
- package/src/router.js +632 -159
- package/src/types.d.ts +16 -0
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +16 -0
- package/src/worker.js +2 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
209
|
-
|
|
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
|
|
256
|
-
super(
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
374
|
-
//
|
|
375
|
-
this.
|
|
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
|
-
|
|
487
|
-
return
|
|
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
|
-
|
|
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 (
|
|
980
|
+
if (rawIp.byteLength === 4) {
|
|
747
981
|
// ipv4
|
|
748
|
-
ip = new Uint8Array(
|
|
982
|
+
ip = new Uint8Array(rawIp).join(".");
|
|
749
983
|
if (mapsIPv4Peer(this.app)) {
|
|
750
984
|
ip = "::ffff:" + ip;
|
|
751
985
|
}
|
|
752
|
-
} else if (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
groups
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|