fulmine.js 5.19.2 → 5.19.3

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,80 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ This file is derived from Ultimate Express and has been modified.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ const { settingsEpoch } = require("./utils.js");
21
+
22
+ // The settings the hot paths read, resolved to plain fields: each read was a variadic get() whose
23
+ // rest array escapes into createRoute, plus a dictionary miss per mount level for the json keys.
24
+ // One shape for every router, stale when the epoch moves.
25
+ class HotSettings {
26
+ /** Every field declared up front, one hidden class for every router's copy. */
27
+ constructor() {
28
+ this.epoch = 0;
29
+ this.xPoweredBy = false;
30
+ this.etagFn = undefined;
31
+ // null means every method, which is express's behaviour and the default
32
+ this.etagMethods = null;
33
+ this.queryParserFn = undefined;
34
+ this.trustProxyFn = undefined;
35
+ this.trustProxyProtocol = false;
36
+ this.jsonEscape = undefined;
37
+ this.jsonReplacer = undefined;
38
+ this.jsonSpaces = undefined;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * What app.settings is wrapped in, so a write that never went through set() still bumps the epoch.
44
+ * Only writes are trapped, a read is the plain operation on the object.
45
+ *
46
+ * defineProperty is here for the trust proxy default marker, which set() writes that way.
47
+ */
48
+ const settingsWriteTraps = {
49
+ /**
50
+ * @param {any} target the settings object itself, whose keys are the application's
51
+ * @param {string|symbol} key
52
+ * @param {any} value whatever the application is setting
53
+ */
54
+ set(target, key, value) {
55
+ target[key] = value;
56
+ settingsEpoch.n++;
57
+ return true;
58
+ },
59
+ /**
60
+ * @param {any} target the settings object itself
61
+ * @param {string|symbol} key
62
+ */
63
+ deleteProperty(target, key) {
64
+ delete target[key];
65
+ settingsEpoch.n++;
66
+ return true;
67
+ },
68
+ /**
69
+ * @param {any} target the settings object itself
70
+ * @param {string|symbol} key
71
+ * @param {any} descriptor
72
+ */
73
+ defineProperty(target, key, descriptor) {
74
+ Object.defineProperty(target, key, descriptor);
75
+ settingsEpoch.n++;
76
+ return true;
77
+ }
78
+ };
79
+
80
+ module.exports = { HotSettings, settingsWriteTraps };
package/src/index.js CHANGED
@@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
17
17
  limitations under the License.
18
18
  */
19
19
 
20
- // H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
21
- // declaration file the package ships, so the module is read through a loose alias
20
+ // H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the .d.ts the
21
+ // package ships, so the module is read through a loose alias
22
22
  const uWS = require("uWebSockets.js");
23
23
  const uWSAny = /** @type {any} */ (uWS);
24
24
  const Application = require("./application.js");
@@ -36,9 +36,8 @@ try {
36
36
  }
37
37
 
38
38
  try {
39
- // the compile cache, in node since 22.8: the next boot of the same code skips compiling it.
40
- // Asked for here because in practice the framework is the entry point of the application
41
- // using it. Respects NODE_DISABLE_COMPILE_CACHE, and booting without a cache is not an error
39
+ // the compile cache, node 22.8 and up: the next boot skips compiling the same code. Respects
40
+ // NODE_DISABLE_COMPILE_CACHE, and booting without a cache is not an error
42
41
  require("node:module").enableCompileCache?.();
43
42
  } catch (error) {
44
43
  // node below 22.8, or a disk the cache cannot be written to
@@ -47,9 +46,8 @@ try {
47
46
  // The factory doubles as a namespace, as in Express: Router, static and the body parsers hang off
48
47
  // the function that creates an app.
49
48
  //
50
- // Written as `module.exports.name = ...` and never through an alias: cjs-module-lexer reads this
51
- // file as text to decide which named exports an ESM importer gets, and it cannot see through one.
52
- // Nothing here is executed to find that out.
49
+ // Always `module.exports.name = ...`, never through an alias: cjs-module-lexer reads this file as
50
+ // text to decide the named exports an ESM importer gets, and it cannot see through an alias.
53
51
  /**
54
52
  * @type {typeof Application & {
55
53
  * Router: Function,
@@ -69,8 +67,7 @@ try {
69
67
  */
70
68
  module.exports = /** @type {any} */ (Application);
71
69
 
72
- // a router is a function too, for the same reason an app is: it has to be callable to be usable as
73
- // middleware
70
+ // a router is a function too: it has to be callable to be used as middleware
74
71
  module.exports.Router = function (options) {
75
72
  return new Router(options)._asCallable();
76
73
  };
@@ -80,18 +77,15 @@ module.exports.Route = Route;
80
77
 
81
78
  module.exports.request = Request.prototype;
82
79
  module.exports.response = Response.prototype;
83
- // the third of the trio: adding a method here adds it to every app, the same as express.application
80
+ // adding a method here adds it to every app, the same as express.application
84
81
  module.exports.application = Application.Application.prototype;
85
82
 
86
83
  module.exports.static = middlewares.static;
87
- // what listen() decided about each route, as something a test can assert on rather than something
88
- // to read in a terminal. See src/testing.js
84
+ // what listen() decided about each route, as something a test can assert on. See src/testing.js
89
85
  module.exports.testing = require("./testing.js");
90
- // not one of express's, since express has none: the compression module is what everyone installs
91
- // instead, and this is that middleware's options and behaviour without the install
86
+ // express has none: this is the compression module's options and behaviour, without the install
92
87
  module.exports.compression = require("./compression.js");
93
- // Server-Timing with the routing verdict in it, which no other framework can report because no
94
- // other framework has two routes to tell apart. See src/server-timing.js
88
+ // Server-Timing with the routing verdict in it. See src/server-timing.js
95
89
  module.exports.serverTiming = require("./server-timing.js");
96
90
  module.exports.json = middlewares.json;
97
91
  module.exports.urlencoded = middlewares.urlencoded;
@@ -0,0 +1,129 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ This file is derived from Ultimate Express and has been modified.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ const { Readable } = require("stream");
21
+
22
+ // 128 KB of body buffered before uWS is asked to pause
23
+ const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
24
+
25
+ /**
26
+ * A Readable that has not been built yet.
27
+ *
28
+ * Every request pays for the stream and almost none use it: a GET carries no body, and the bodies
29
+ * that arrive are collected by uWS and handed to the parsers without touching the stream. Measured
30
+ * on this machine, Readable's constructor is about 90ns of the 900ns a hello-world request costs.
31
+ *
32
+ * So the chain says Readable and the constructor does not run. `Request extends LazyReadable`, and
33
+ * LazyReadable's prototype is Readable's, so `req instanceof Readable` stays true and every
34
+ * Readable method is reachable. What is missing is `_readableState`, built on the first touch.
35
+ *
36
+ * The wrapping below is generated, not written out: every own member of Readable's prototype gets a
37
+ * version that materialises first, so there is no list to keep in step. Missing one would be a
38
+ * TypeError on `undefined._readableState`, not a slow path.
39
+ */
40
+ class LazyReadableBase {}
41
+ Object.setPrototypeOf(LazyReadableBase.prototype, Readable.prototype);
42
+ Object.setPrototypeOf(LazyReadableBase, Readable);
43
+
44
+ // what the chain says at runtime, said again for the type checker, which cannot see a prototype
45
+ // being reassigned: everything a Readable offers is reachable from a Request, and is a Readable's
46
+ const LazyReadable = /** @type {typeof Readable} */ (/** @type {unknown} */ (LazyReadableBase));
47
+
48
+ /**
49
+ * Builds the stream this object has been pretending to be. Idempotent: everything that can be
50
+ * reached from outside goes through it, so it is called far more often than it does anything.
51
+ *
52
+ * EventEmitter's init keeps an _events that is already there, so listeners added before this
53
+ * survive it.
54
+ *
55
+ * @param {any} stream the Request pretending to be one, before its state exists
56
+ */
57
+ function materialise(stream) {
58
+ if (stream._readableState === undefined) {
59
+ Readable.call(stream, READABLE_OPTIONS);
60
+ }
61
+ }
62
+
63
+ for (const member of [
64
+ ...Object.getOwnPropertyNames(Readable.prototype),
65
+ ...Object.getOwnPropertySymbols(Readable.prototype)
66
+ ]) {
67
+ // the constructor is not a door, and `readable` is handled below because a request writes it
68
+ // and writing it must not build the very thing this is avoiding
69
+ if (member === "constructor" || member === "readable") {
70
+ continue;
71
+ }
72
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Readable.prototype, member));
73
+ if (typeof descriptor.value === "function") {
74
+ const inner = descriptor.value;
75
+ Object.defineProperty(LazyReadableBase.prototype, member, {
76
+ ...descriptor,
77
+ /** @this {any} @param {...any} args */
78
+ value: function (...args) {
79
+ materialise(this);
80
+ return inner.apply(this, args);
81
+ }
82
+ });
83
+ } else if (descriptor.get || descriptor.set) {
84
+ const innerGet = descriptor.get;
85
+ const innerSet = descriptor.set;
86
+ Object.defineProperty(LazyReadableBase.prototype, member, {
87
+ ...descriptor,
88
+ get: innerGet
89
+ ? /** @this {any} */ function () {
90
+ materialise(this);
91
+ return innerGet.call(this);
92
+ }
93
+ : undefined,
94
+ set: innerSet
95
+ ? /** @this {any} @param {any} value */ function (value) {
96
+ materialise(this);
97
+ innerSet.call(this, value);
98
+ }
99
+ : undefined
100
+ });
101
+ }
102
+ }
103
+
104
+ const nodeReadable = /** @type {PropertyDescriptor} */ (
105
+ Object.getOwnPropertyDescriptor(Readable.prototype, "readable")
106
+ );
107
+
108
+ // `readable` on its own: a request sets it while it is being built, and node's setter is a no-op
109
+ // without the state anyway, so the flag is kept as a plain field until there is a stream to ask
110
+ Object.defineProperty(LazyReadableBase.prototype, "readable", {
111
+ configurable: true,
112
+ enumerable: false,
113
+ /** @this {any} */
114
+ get: function () {
115
+ return this._readableState === undefined
116
+ ? this._readableFlag === true
117
+ : /** @type {any} */ (nodeReadable.get).call(this);
118
+ },
119
+ /** @this {any} @param {any} value */
120
+ set: function (value) {
121
+ if (this._readableState === undefined) {
122
+ this._readableFlag = !!value;
123
+ return;
124
+ }
125
+ /** @type {any} */ (nodeReadable.set).call(this, value);
126
+ }
127
+ });
128
+
129
+ module.exports = { LazyReadable, READABLE_OPTIONS };
@@ -0,0 +1,97 @@
1
+ /*
2
+ Copyright 2024 dimden.dev
3
+ Copyright 2026 Nigro Simone
4
+
5
+ This file is derived from Ultimate Express and has been modified.
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+ */
19
+
20
+ const { Writable } = require("stream");
21
+
22
+ /**
23
+ * A Writable that has not built its state yet, the mirror of LazyReadable and for the same reason:
24
+ * a response is a Writable because middleware expects one, and the ordinary one never uses it.
25
+ * `send()` reaches `end()`, which is overridden here and goes straight to _finish, so the
26
+ * WritableState was allocated for every response and read by nobody. Only res.write(), a pipe,
27
+ * cork and the writableX getters need it.
28
+ *
29
+ * `Response extends LazyWritable`, whose prototype is Writable's, so `res instanceof Writable`
30
+ * stays true. `_writableState` is built on the first touch. Measured at 45ns a response.
31
+ *
32
+ * As on the Readable side the wrapping is generated: every own member of Writable's prototype gets
33
+ * a version that materialises first, so there is no list to keep in step.
34
+ */
35
+ class LazyWritableBase {}
36
+ Object.setPrototypeOf(LazyWritableBase.prototype, Writable.prototype);
37
+ Object.setPrototypeOf(LazyWritableBase, Writable);
38
+
39
+ // what the chain says at runtime, said again for the type checker, which cannot see a prototype
40
+ // being reassigned
41
+ const LazyWritable = /** @type {typeof Writable} */ (/** @type {unknown} */ (LazyWritableBase));
42
+
43
+ /**
44
+ * Builds the stream this object has been pretending to be. Idempotent: everything reachable from
45
+ * outside goes through it, so it is called far more often than it does anything.
46
+ *
47
+ * EventEmitter's init keeps an _events that is already there, so both the shape the constructor
48
+ * wrote and any listener added before this survive it.
49
+ *
50
+ * @param {any} stream the Response pretending to be one, before its state exists
51
+ */
52
+ function materialiseWritable(stream) {
53
+ if (stream._writableState === undefined) {
54
+ Writable.call(stream);
55
+ }
56
+ }
57
+
58
+ for (const member of [
59
+ ...Object.getOwnPropertyNames(Writable.prototype),
60
+ ...Object.getOwnPropertySymbols(Writable.prototype)
61
+ ]) {
62
+ if (member === "constructor") {
63
+ continue;
64
+ }
65
+ const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Writable.prototype, member));
66
+ if (typeof descriptor.value === "function") {
67
+ const inner = descriptor.value;
68
+ Object.defineProperty(LazyWritableBase.prototype, member, {
69
+ ...descriptor,
70
+ /** @this {any} @param {...any} args */
71
+ value: function (...args) {
72
+ materialiseWritable(this);
73
+ return inner.apply(this, args);
74
+ }
75
+ });
76
+ } else if (descriptor.get || descriptor.set) {
77
+ const innerGet = descriptor.get;
78
+ const innerSet = descriptor.set;
79
+ Object.defineProperty(LazyWritableBase.prototype, member, {
80
+ ...descriptor,
81
+ get: innerGet
82
+ ? /** @this {any} */ function () {
83
+ materialiseWritable(this);
84
+ return innerGet.call(this);
85
+ }
86
+ : undefined,
87
+ set: innerSet
88
+ ? /** @this {any} @param {any} value */ function (value) {
89
+ materialiseWritable(this);
90
+ innerSet.call(this, value);
91
+ }
92
+ : undefined
93
+ });
94
+ }
95
+ }
96
+
97
+ module.exports = { LazyWritable };
@@ -72,17 +72,18 @@ const PRECOMPRESSED = [
72
72
  { encoding: "gzip", suffix: ".gz", flag: ENCODING_GZIP }
73
73
  ];
74
74
 
75
+ /** @typedef {import("./request.js")} Request */
76
+ /** @typedef {import("./response.js")} Response */
77
+
75
78
  // The failures express.static answers by moving on to the next handler rather than by reporting
76
79
  // them, when fallthrough is on. They all mean the same thing: the request is not a file here.
77
80
  //
78
- // serve-static decides this by remembering whether send got as far as settling on a file, and
79
- // forwards everything after that point. The list is the same thing said from the other side, since
80
- // by the time this hands over, the file has been found and stat'ed already: what is left to fail
81
- // is a dotfile rule or a path that will not decode.
81
+ // serve-static decides this by remembering whether send got as far as settling on a file. The list
82
+ // says the same from the other side: by the time this hands over the file has been found and
83
+ // stat'ed, so what is left to fail is a dotfile rule or a path that will not decode.
82
84
  //
83
- // A 412 and a 416 are not on it, and that is the point of the list. Both are about a file that
84
- // exists and about conditions the client itself set, and falling through swallowed them: a Range
85
- // Not Satisfiable came back as a 404, which tells the client its file is gone when it is not.
85
+ // A 412 and a 416 are not on it. Both are about a file that exists and about conditions the client
86
+ // itself set, and falling through swallowed them: a Range Not Satisfiable came back as a 404.
86
87
  const FALLTHROUGH_STATUSES = new Set([400, 403, 404]);
87
88
 
88
89
  /**
@@ -207,10 +208,10 @@ function decodeBody(buf, encoding) {
207
208
  * Runs the caller's verify hook the way body-parser does, an empty body included; a throw becomes
208
209
  * the 403 entity.verify.failed error. Answers whether parsing may continue.
209
210
  *
210
- * @param {any} req
211
- * @param {any} res
211
+ * @param {Request} req
212
+ * @param {Response} res
212
213
  * @param {(err?: any) => void} next
213
- * @param {any} options
214
+ * @param {any} options the parser options, settled by createBodyParser
214
215
  * @param {Buffer} buf
215
216
  * @returns {boolean}
216
217
  */
@@ -236,10 +237,9 @@ function runVerify(req, res, next, options, buf) {
236
237
  * The message a strict violation gets, which is the one V8 would have produced had the body been
237
238
  * invalid JSON rather than merely not an object.
238
239
  *
239
- * body-parser goes to some trouble over this: it builds a string that is the body up to the
240
- * offending character followed by placeholder characters, asks JSON.parse to fail on that, and
241
- * then puts the real characters back into whatever V8 said. The point is that an application
242
- * showing err.message reads the same sentence either way, naming the character and its position.
240
+ * body-parser builds a string that is the body up to the offending character followed by
241
+ * placeholders, asks JSON.parse to fail on that, and puts the real characters back into what V8
242
+ * said, so an application showing err.message reads the same sentence either way.
243
243
  *
244
244
  * @param {string} text the body as sent
245
245
  * @param {string|undefined} char the first character that is neither whitespace nor { nor [
@@ -296,7 +296,7 @@ function bodyError(message, status, type, extra) {
296
296
  * its stack and any property the thrower put on it are all still there when the application
297
297
  * reads it.
298
298
  *
299
- * @param {any} err
299
+ * @param {any} err whatever was thrown, which need not be an Error
300
300
  * @param {number} status
301
301
  * @param {string} type body-parser's own name for the kind of failure
302
302
  * @param {object} [extra] anything else body-parser puts on that particular error
@@ -361,12 +361,12 @@ function twinsOf(filePath, ttl) {
361
361
  * The compressed twin of a file to serve in its place, or undefined when the client would rather
362
362
  * have the file itself or the twin is not there.
363
363
  *
364
- * The stat comes back with it, and is what sendFile then answers from: the ETag and the
365
- * Last-Modified of a variant are its own, which is the whole point. Two bodies sharing one ETag is
366
- * how a shared cache ends up handing brotli to a client that cannot read it.
364
+ * The stat comes back with it, and is what sendFile then answers from: the ETag and Last-Modified
365
+ * of a variant are its own. Two bodies sharing one ETag is how a shared cache ends up handing
366
+ * brotli to a client that cannot read it.
367
367
  *
368
- * One stat when the answer is a twin, and none at all when the last request already found there is
369
- * no twin to have. See twinCache above for what is remembered and what is not.
368
+ * One stat when the answer is a twin, none when the last request already found there is no twin.
369
+ * See twinCache above for what is remembered.
370
370
  *
371
371
  * @param {string} filePath absolute path of the file that was asked for
372
372
  * @param {string|undefined} accept the request's Accept-Encoding
@@ -555,12 +555,11 @@ function serveStatic(root, options) {
555
555
  // Joined against the root and not normalised on its own first, which is the difference
556
556
  // between "/mount/../package.json" being refused and being served: a ".." has to climb
557
557
  // relative to the root so the check below can see it leave, and normalizing the url alone
558
- // clamps it at "/" where nothing has left anywhere. Absolute because resolvedRoot is, so
559
- // nothing here resolves against the working directory per request either.
560
- // and without the trailing separator join keeps and resolve does not, because statTarget
561
- // below puts it back only where it belongs: linux refuses a file asked for as a directory,
562
- // so a mount whose root is a file answers nothing at all if the separator stays here.
563
- // Windows stats it either way, which is why only the CI said so.
558
+ // clamps it at "/". Absolute because resolvedRoot is, so nothing resolves against the
559
+ // working directory per request.
560
+ // Without the trailing separator join keeps and resolve does not, because statTarget below
561
+ // puts it back only where it belongs: linux refuses a file asked for as a directory, so a
562
+ // mount whose root is a file would answer nothing. Windows stats it either way
564
563
  let fullpath = path.join(resolvedRoot, url);
565
564
  if (fullpath.length > resolvedRoot.length && fullpath.endsWith(path.sep)) {
566
565
  fullpath = fullpath.slice(0, -1);
@@ -570,12 +569,11 @@ function serveStatic(root, options) {
570
569
  let filePath = fullpath;
571
570
  // What serve-static hands send is this path, except that a bare "/" under a mount the
572
571
  // request did not write with one becomes "": without that rule a mount whose root is a file
573
- // would ask the disk for a directory and could never answer at all.
572
+ // would ask the disk for a directory and could never answer.
574
573
  //
575
- // Send then stats `normalize(join(root, path))`, and both of those keep a trailing
576
- // separator where `resolve` takes it off. The separator is not decoration: the disk refuses
577
- // a file that is asked for as a directory, and the name inside the error carries it, which
578
- // is what an error handler prints when fallthrough is off.
574
+ // Send then stats `normalize(join(root, path))`, and both keep a trailing separator where
575
+ // `resolve` takes it off. The disk refuses a file asked for as a directory, and the name
576
+ // inside the error carries it, which is what an error handler prints
579
577
  const mountRelative = rawPath === "/" && !req.endsWithSlash ? "" : url;
580
578
  const statTarget = mountRelative.endsWith("/") && !fullpath.endsWith(path.sep) ? fullpath + path.sep : fullpath;
581
579
  if (root && !fullpath.startsWith(resolvedRoot)) {
@@ -586,14 +584,10 @@ function serveStatic(root, options) {
586
584
  }
587
585
 
588
586
  // Before the stat, because send judges the path before it looks at the disk: a hidden
589
- // segment anywhere in a path that does not exist answers what the dotfiles rule says and
590
- // not the ENOENT the disk would have given. sendFile applies the same rule below, and
591
- // reaches it only for paths that do exist.
592
- // normalized first, as send normalizes before it judges: a ".." segment is not a hidden
593
- // file, and resolving it away is what tells the two apart
594
- // and these are the segments path.normalize(url) would have produced, taken off the joined
595
- // path rather than walked again: the check above has just proved it starts with the root,
596
- // so what follows the root is the url in normal form
587
+ // segment in a path that does not exist answers what the dotfiles rule says and not the
588
+ // ENOENT the disk would have given. Normalized first, as send normalizes before it judges:
589
+ // a ".." segment is not a hidden file. These are the segments path.normalize(url) would
590
+ // have produced, taken off the joined path rather than walked again
597
591
  if (containsDotFile(fullpath.slice(resolvedRoot.length).split(/[\\/]/))) {
598
592
  const refusal = options.dotfiles === "deny" ? 403 : options.dotfiles === "allow" ? 0 : 404;
599
593
  if (refusal !== 0 && !(options.dotfiles === "ignore_files" && !path.basename(url).startsWith("."))) {
@@ -689,13 +683,11 @@ function serveStatic(root, options) {
689
683
  if (stat.isDirectory() || req.endsWithSlash) {
690
684
  if (!req.endsWithSlash) {
691
685
  if (options.redirect) {
692
- // The query goes along, and the leading slashes are collapsed. Both were
693
- // wrong: "/docs?page=3" redirected to "/docs/" and lost the page, and a
694
- // request for "//assets" answered "Location: //assets/", which a browser
695
- // reads as a protocol-relative URL and follows to the host "assets". A
696
- // redirect that leaves this server is not a redirect this server meant.
697
- // serve-static locks its redirect page down the way it locks an error page:
698
- // the body names the target, and the target came from the request
686
+ // The query goes along and the leading slashes are collapsed. Both were wrong:
687
+ // "/docs?page=3" redirected to "/docs/" and lost the page, and "//assets"
688
+ // answered "Location: //assets/", which a browser reads as protocol-relative
689
+ // and follows to the host "assets". serve-static locks its redirect page down
690
+ // the way it locks an error page: the body names a target the request supplied
699
691
  res.setHeader("Content-Security-Policy", "default-src 'none'");
700
692
  res.setHeader("X-Content-Type-Options", "nosniff");
701
693
  return res.redirect(301, collapseLeadingSlashes(req._originalPath + "/") + req.urlQuery, true);
@@ -813,10 +805,9 @@ function createInflate(contentEncoding) {
813
805
  }
814
806
 
815
807
  /**
816
- * Builds one of the body parsers. All four share the same work, which is deciding whether this
817
- * request has a body worth reading, collecting it within the size limit, decompressing it and
818
- * handing the bytes over; they differ only in the content type they claim by default and in what
819
- * they turn the bytes into.
808
+ * Builds one of the body parsers. All four share the same work: deciding whether this request has
809
+ * a body worth reading, collecting it within the size limit, decompressing it and handing the
810
+ * bytes over. They differ in the content type they claim and in what they turn the bytes into.
820
811
  *
821
812
  * @param {string} defaultType the type matched when the caller names none
822
813
  * @param {(...args: any[]) => any} beforeReturn turns the collected bytes into req.body. Called
@@ -881,12 +872,12 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
881
872
  // Whether a content-type is one this parser claims, remembered per parser.
882
873
  //
883
874
  // Only reached when the caller asked for a wildcard or a list, since a plain type takes the
884
- // simpleType shortcut above and never calls type-is at all. For those callers type-is was
885
- // 513 ns to reach the same answer about the same string on every request, against 4 ns for
886
- // an answer already worked out. The header is the client's, so the memo needs its ceiling.
875
+ // simpleType shortcut above. For those callers type-is was 513ns to reach the same answer
876
+ // about the same string on every request, against 4ns for one already worked out. The
877
+ // header is the client's, so the memo needs its ceiling.
887
878
  //
888
879
  // typeis.is and not typeis(req, ...): the request form first checks that there is a body,
889
- // and the caller below has established that already.
880
+ // which the caller below has established.
890
881
  const claimsType = memoizeByString(
891
882
  (contentType) => !!typeis.is(contentType, /** @type {string[]} */ (options.type))
892
883
  );
@@ -908,12 +899,10 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
908
899
  }
909
900
 
910
901
  // The property goes on the request before anything is decided, and its value stays
911
- // undefined: body-parser's read() does exactly this, and the two halves both matter.
912
- // Undefined, so a handler can still tell "nothing parsed this" from "the body was
913
- // empty", which seeding an empty object would lose. Present, because `"body" in req`
914
- // is how a library asks whether a parser has run at all: Apollo's express middleware
915
- // refuses the request with a 500 when the property is missing, and tRPC's adapter
916
- // reads the body itself when it is, so getting either half wrong breaks one of them.
902
+ // undefined: body-parser's read() does the same, and both halves matter. Undefined, so
903
+ // a handler can tell "nothing parsed this" from "the body was empty". Present, because
904
+ // `"body" in req` is how a library asks whether a parser has run: Apollo's express
905
+ // middleware answers 500 when it is missing, and tRPC's adapter reads the body itself
917
906
  if (!("body" in req)) {
918
907
  req.body = undefined;
919
908
  }
@@ -1013,7 +1002,6 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1013
1002
  return next();
1014
1003
  }
1015
1004
 
1016
- const abs = [];
1017
1005
  let inflate;
1018
1006
  let totalSize = 0;
1019
1007
  const rawContentEncoding = req._rawHeader("content-encoding");
@@ -1047,11 +1035,9 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1047
1035
  next = bindContext(next);
1048
1036
 
1049
1037
  // with nothing to decompress, uWS can collect the whole body in native code: one
1050
- // callback instead of one per chunk, the limit enforced before any byte reaches JS,
1051
- // and no copy at all - the parsers turn the bytes into req.body before the callback
1052
- // returns, so a view over uWS's own memory is enough. A declared length was the
1053
- // original case; a chunked body accumulates in the same native vector and only loses
1054
- // the length check, since there is no declaration to hold it to
1038
+ // callback instead of one per chunk, the limit enforced before any byte reaches JS, and
1039
+ // no copy at all, since the parsers turn the bytes into req.body before the callback
1040
+ // returns. A chunked body uses the same native vector and only loses the length check
1055
1041
  const declared = lengthNumber;
1056
1042
  const declaresLength = !Number.isNaN(declared) && declared > 0;
1057
1043
  if (!req.receivedData && !inflate && req._res.collectBody && (declaresLength || isNaN(declared))) {
@@ -1093,12 +1079,11 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1093
1079
  }
1094
1080
 
1095
1081
  // uWS neuters its ArrayBuffer after the callback, so every chunk has to be copied out of
1096
- // it - and then Buffer.concat copied the whole body a second time. when content-length is
1097
- // known and we aren't inflating, the final size is known up front, so chunks can go
1098
- // straight into one buffer and the body is copied once.
1099
- // the cap means a client that declares a body and never sends it costs no more than one
1100
- // that actually sends a body that size, and content-length above limit was
1101
- // already rejected above
1082
+ // it, and then Buffer.concat copied the whole body a second time. When content-length is
1083
+ // known and we are not inflating, the final size is known up front, so chunks go
1084
+ // straight into one buffer and the body is copied once. The cap means a client that
1085
+ // declares a body and never sends it costs no more than one that sends it
1086
+ const abs = [];
1102
1087
  const declaredLength = inflate ? -1 : Number(length);
1103
1088
  let target =
1104
1089
  declaredLength > 0 && declaredLength <= MAX_PREALLOCATED_BODY
@@ -1116,11 +1101,10 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
1116
1101
  /**
1117
1102
  * A zlib throw becomes the 400 body-parser answers a corrupt body with.
1118
1103
  *
1119
- * zlib reports it twice: process() throws, and the stream emits 'error' a tick
1120
- * later. fast-zlib removes its own listeners on the way out, so that second one
1121
- * lands on nothing, and an unhandled 'error' event ends the process: a corrupt
1122
- * gzip body was enough to take the server down. The listener goes on after the
1123
- * throw, since process() would have removed it.
1104
+ * zlib reports it twice: process() throws, and the stream emits 'error' a tick later.
1105
+ * fast-zlib removes its own listeners on the way out, so that second one lands on
1106
+ * nothing, and an unhandled 'error' event ends the process. The listener goes on after
1107
+ * the throw, since process() would have removed it.
1124
1108
  *
1125
1109
  * @param {any} err what inflate.process threw
1126
1110
  */