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.
- package/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +46 -45
- package/src/cli.js +28 -34
- package/src/cluster.js +16 -25
- package/src/compression.js +39 -51
- package/src/declarative.js +586 -538
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +129 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +61 -77
- package/src/nest.js +19 -34
- package/src/node-shim.js +11 -13
- package/src/optimizer.js +598 -0
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +306 -0
- package/src/request.js +101 -513
- package/src/response-utils.js +88 -0
- package/src/response.js +100 -443
- package/src/route.js +4 -5
- package/src/router-utils.js +950 -0
- package/src/router.js +126 -2148
- package/src/server-shape.js +26 -41
- package/src/server-timing.js +16 -29
- package/src/socket.js +208 -0
- package/src/testing.js +39 -42
- package/src/usage.js +16 -21
- package/src/utils.js +49 -59
- package/src/verify.js +18 -28
- package/src/view.js +5 -7
- package/src/walk.js +580 -0
- package/src/websocket.js +19 -20
- package/src/work.js +21 -27
|
@@ -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
|
|
21
|
-
//
|
|
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,
|
|
40
|
-
//
|
|
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
|
-
//
|
|
51
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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 };
|
package/src/middlewares.js
CHANGED
|
@@ -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
|
|
79
|
-
//
|
|
80
|
-
//
|
|
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
|
|
84
|
-
//
|
|
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 {
|
|
211
|
-
* @param {
|
|
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
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
|
365
|
-
*
|
|
366
|
-
*
|
|
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,
|
|
369
|
-
*
|
|
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 "/"
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
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
|
|
572
|
+
// would ask the disk for a directory and could never answer.
|
|
574
573
|
//
|
|
575
|
-
// Send then stats `normalize(join(root, path))`, and both
|
|
576
|
-
//
|
|
577
|
-
//
|
|
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
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
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
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
//
|
|
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
|
|
817
|
-
*
|
|
818
|
-
*
|
|
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
|
|
885
|
-
//
|
|
886
|
-
//
|
|
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
|
-
//
|
|
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
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
//
|
|
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
|
-
//
|
|
1052
|
-
// returns
|
|
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
|
|
1097
|
-
// known and we
|
|
1098
|
-
// straight into one buffer and the body is copied once.
|
|
1099
|
-
//
|
|
1100
|
-
|
|
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
|
-
*
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
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
|
*/
|