fulmine.js 5.11.1 → 5.12.1
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/README.md +9 -3
- package/package.json +15 -16
- package/src/application.js +57 -40
- package/src/cli.js +39 -4
- package/src/declarative.js +25 -20
- package/src/index.js +9 -0
- package/src/middlewares.js +18 -10
- package/src/request.js +83 -9
- package/src/response.js +76 -32
- package/src/router.js +198 -55
- package/src/utils.js +74 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Fulmine.js
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Fulmine - means lightning ⚡ in Italian - is a drop-in replacement for Express 5, running on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js) instead of `node:http`. Your existing middleware keeps working.
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
8
|
const express = require("fulmine.js"); // instead of require("express")
|
|
@@ -29,9 +29,12 @@ npx fulmine.js explain /api/items # what happens when a request for that route
|
|
|
29
29
|
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
30
30
|
|
|
31
31
|
[](https://www.npmjs.com/package/fulmine.js)
|
|
32
|
-
[](https://nodejs.org)
|
|
33
|
+
[](https://www.http-arena.com/#tuned=0)
|
|
33
34
|
[](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
|
|
34
35
|
[](https://github.com/nigrosimone/fulmine.js/actions/workflows/codeql.yml)
|
|
36
|
+
[](https://scorecard.dev/viewer/?uri=github.com/nigrosimone/fulmine.js)
|
|
37
|
+
[](https://www.bestpractices.dev/projects/14089)
|
|
35
38
|
[](./LICENSE)
|
|
36
39
|
|
|
37
40
|
## Table of contents
|
|
@@ -282,6 +285,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
282
285
|
- `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`. Runnable: [`examples/graceful-shutdown.js`](./examples/graceful-shutdown.js).
|
|
283
286
|
- `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
|
|
284
287
|
- request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
|
|
288
|
+
- **A compiled route answers `connection: keep-alive` to a client that sent `Connection: close`.** A handler simple enough to be read at registration time is answered by µWS from a response written once at `listen()`, and that response cannot read the request. The socket still closes, so what is wrong is the header and not the transport. A response that would carry a validator is never compiled, so conditional requests behave as on Express; `app.set("declarative responses", false)` turns compiling off.
|
|
285
289
|
- **Informational responses go nowhere.** `res.writeEarlyHints()`, `res.writeContinue()` and `res.writeProcessing()` are all there, take what node's take and throw what node's throw once the head has gone out, but nothing reaches the wire: µWebSockets.js has no API for a `1xx`. They exist so that code written for Express keeps running rather than dying on "is not a function", which is the only thing a drop-in can honestly promise here. `res.addTrailers()` is the same story, and `res.setTimeout()` and `req.setTimeout()` register the listener without changing anything, since µWS runs its own idle timeout through `uwsOptions.idleTimeout`.
|
|
286
290
|
- For HTTPS, instead of doing this:
|
|
287
291
|
|
|
@@ -325,6 +329,7 @@ app.listen(3000, () => {
|
|
|
325
329
|
Runnable: [`examples/https.js`](./examples/https.js).
|
|
326
330
|
|
|
327
331
|
- This also applies to non-SSL HTTP too. Use `app.listen()` rather than creating a server by hand. `http.createServer(app)` does work, because the app is a request listener like Express's and answers node's requests through a shim, which is what lets `supertest`, `vhost` and anything else that calls an app keep working. But it serves those requests through `node:http` rather than through µWS, so the speed is Express's. It is there for compatibility, not for production.
|
|
332
|
+
- **Node 22, 24 and 26, not every version above 22.** µWebSockets.js ships one prebuilt binary per Node ABI and skips the odd lines, so Node 23 and 25 have no binary to load and fail at `require`. `npx fulmine.js verify` says which binary this machine wants and whether it is there. The odd/even model ends with Node 26, so the gap closes on its own.
|
|
328
333
|
- Node.JS max header size is 16384 bytes, while uWebSockets by default is 4096 bytes, so if you need longer headers set the env variable `UWS_HTTP_MAX_HEADERS_SIZE` to max byte count you need.
|
|
329
334
|
- uWebSockets drops a request whose body arrives slower than 16KB/s, and the timeout is not reachable from JavaScript, while Node.JS waits as long as the client needs. Uploads over very slow connections can therefore fail here and succeed on Express. A body stalled for 5 seconds still completes; one stalled for 12 seconds gets its socket reset at around 11.8 seconds.
|
|
330
335
|
|
|
@@ -700,8 +705,9 @@ Two of these keep a compiled form alongside the value, which you can also set di
|
|
|
700
705
|
- `etag fn`, the function that produces an ETag. Setting `etag` compiles one; setting this replaces it.
|
|
701
706
|
- `query parser fn`, likewise for `query parser`.
|
|
702
707
|
|
|
703
|
-
Fulmine adds
|
|
708
|
+
Fulmine adds six of its own:
|
|
704
709
|
|
|
710
|
+
- `etag methods`, unset by default. Express computes the generated ETag for every method, and so does this until told otherwise. `app.set("etag methods", ["GET", "HEAD"])` skips the digest on every other method, where freshness is not defined and the validator can never match: worth 21% here on a 4KB POST answer. An ETag set by hand still goes out whatever the method.
|
|
705
711
|
- `declarative responses`, on by default. Lets a simple enough handler be compiled into a native uWS response, described under [Performance tips](#performance-tips).
|
|
706
712
|
- `connection headers`, on by default. Express sends `Connection: keep-alive` and `Keep-Alive` on every response, and so does this. Turn it off and neither goes out, while a connection the client asked to close still answers `Connection: close`: it is the advertisement that goes, not the truth. Worth 2% to 3.5% here on a route that is not compiled, plus the bytes.
|
|
707
713
|
- `file cache`, on by default. Small files served by `res.sendFile` come from a bounded in-process cache, checked against the file's `stat` on every request, so an edited file is never served stale. Turn it off where every request has to reach the disk, which is what a public benchmark asks of a standard entry: it was worth about 4% on a 4KB file here, so the cost of turning it off is small.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.12.1",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node tests/index.js",
|
|
11
|
-
"test:unit": "node --test \"tests/unit/*.test.js\"",
|
|
11
|
+
"test:unit": "node --test --test-timeout=120000 \"tests/unit/*.test.js\"",
|
|
12
12
|
"test:types": "tsd --files tests/types/*.test-d.ts",
|
|
13
13
|
"test:express": "node tools/express-suite.js",
|
|
14
14
|
"fuzz": "node tools/fuzz.js",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@types/express": "^5.0.6",
|
|
70
70
|
"accepts": "^2.0.0",
|
|
71
|
-
"acorn": "^8.
|
|
71
|
+
"acorn": "^8.18.0",
|
|
72
72
|
"bytes": "^3.1.2",
|
|
73
73
|
"compressible": "^2.0.18",
|
|
74
74
|
"content-disposition": "^1.1.0",
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
"mime-types": "^3.0.2",
|
|
83
83
|
"ms": "^2.1.3",
|
|
84
84
|
"proxy-addr": "^2.0.7",
|
|
85
|
-
"qs": "^6.15.
|
|
86
|
-
"range-parser": "^1.
|
|
85
|
+
"qs": "^6.15.3",
|
|
86
|
+
"range-parser": "^1.3.0",
|
|
87
87
|
"statuses": "^2.0.2",
|
|
88
88
|
"tseep": "^1.3.1",
|
|
89
89
|
"type-is": "^2.1.0",
|
|
@@ -91,7 +91,6 @@
|
|
|
91
91
|
"vary": "^1.1.2"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@codechecks/client": "^0.1.12",
|
|
95
94
|
"@commitlint/cli": "^21.2.1",
|
|
96
95
|
"@commitlint/config-conventional": "^21.2.0",
|
|
97
96
|
"@eslint/js": "^10.0.1",
|
|
@@ -105,7 +104,7 @@
|
|
|
105
104
|
"@types/fresh": "^0.5.3",
|
|
106
105
|
"@types/mime-types": "^3.0.1",
|
|
107
106
|
"@types/ms": "^2.1.0",
|
|
108
|
-
"@types/node": "^
|
|
107
|
+
"@types/node": "^26.2.0",
|
|
109
108
|
"@types/proxy-addr": "^2.0.3",
|
|
110
109
|
"@types/statuses": "^2.0.6",
|
|
111
110
|
"@types/type-is": "^1.6.7",
|
|
@@ -117,21 +116,21 @@
|
|
|
117
116
|
"cookie-parser": "^1.4.7",
|
|
118
117
|
"cookie-session": "^2.1.1",
|
|
119
118
|
"cors": "^2.8.6",
|
|
120
|
-
"ejs": "^
|
|
119
|
+
"ejs": "^6.0.1",
|
|
121
120
|
"errorhandler": "^1.5.2",
|
|
122
121
|
"eslint": "^10.8.0",
|
|
123
122
|
"eslint-config-prettier": "^10.1.8",
|
|
124
|
-
"eslint-plugin-jsdoc": "^
|
|
123
|
+
"eslint-plugin-jsdoc": "^64.1.0",
|
|
125
124
|
"etag": "^1.8.1",
|
|
126
|
-
"eventsource": "^
|
|
127
|
-
"exit-hook": "^
|
|
125
|
+
"eventsource": "^5.0.0",
|
|
126
|
+
"exit-hook": "^5.1.0",
|
|
128
127
|
"express": "^5",
|
|
129
128
|
"express-art-template": "^1.0.1",
|
|
130
129
|
"express-basic-auth": "^1.2.1",
|
|
131
130
|
"express-dot-engine": "^1.0.8",
|
|
132
131
|
"express-fast-json-stringify": "^1.3.0",
|
|
133
132
|
"express-fileupload": "^1.5.2",
|
|
134
|
-
"express-handlebars": "^
|
|
133
|
+
"express-handlebars": "^9.0.1",
|
|
135
134
|
"express-http-proxy": "^2.1.2",
|
|
136
135
|
"express-mongo-sanitize": "^2.2.0",
|
|
137
136
|
"express-rate-limit": "^8.5.2",
|
|
@@ -142,20 +141,20 @@
|
|
|
142
141
|
"globals": "^17.8.0",
|
|
143
142
|
"graphql-http": "^1.22.4",
|
|
144
143
|
"helmet": "^8.2.0",
|
|
145
|
-
"http-proxy-middleware": "^
|
|
144
|
+
"http-proxy-middleware": "^4.2.0",
|
|
146
145
|
"husky": "^9.1.7",
|
|
147
146
|
"lint-staged": "^17.3.0",
|
|
148
147
|
"method-override": "^3.0.0",
|
|
149
148
|
"morgan": "^1.11.0",
|
|
150
149
|
"multer": "^2.1.1",
|
|
151
150
|
"mustache-express": "^1.3.2",
|
|
152
|
-
"nyc": "^
|
|
151
|
+
"nyc": "^18.0.0",
|
|
153
152
|
"on-finished": "^2.4.1",
|
|
154
153
|
"on-headers": "^1.1.0",
|
|
155
|
-
"pako": "^
|
|
154
|
+
"pako": "^3.0.1",
|
|
156
155
|
"passport": "^0.7.0",
|
|
157
156
|
"passport-local": "^1.0.0",
|
|
158
|
-
"pkg-pr-new": "^0.0.
|
|
157
|
+
"pkg-pr-new": "^0.0.87",
|
|
159
158
|
"prettier": "^3.9.6",
|
|
160
159
|
"pug": "^3.0.4",
|
|
161
160
|
"release-it": "^21.0.1",
|
package/src/application.js
CHANGED
|
@@ -26,7 +26,8 @@ const {
|
|
|
26
26
|
createETagGenerator,
|
|
27
27
|
fastQueryParse,
|
|
28
28
|
durationSetting,
|
|
29
|
-
NullObject
|
|
29
|
+
NullObject,
|
|
30
|
+
settingsEpoch
|
|
30
31
|
} = require("./utils.js");
|
|
31
32
|
const parseQuery = require("./parse-query.js");
|
|
32
33
|
const Request = require("./request.js");
|
|
@@ -207,11 +208,11 @@ class Application extends Router {
|
|
|
207
208
|
// a "trust proxy" this app never set is inherited from the parent, as express does:
|
|
208
209
|
// the defaults are deleted so get() falls through to the parent's value
|
|
209
210
|
if (
|
|
210
|
-
this.
|
|
211
|
-
typeof parent.
|
|
211
|
+
this._settings[trustProxyDefaultSymbol] === true &&
|
|
212
|
+
typeof parent._settings["trust proxy fn"] === "function"
|
|
212
213
|
) {
|
|
213
|
-
delete this.
|
|
214
|
-
delete this.
|
|
214
|
+
delete this._settings["trust proxy"];
|
|
215
|
+
delete this._settings["trust proxy fn"];
|
|
215
216
|
}
|
|
216
217
|
});
|
|
217
218
|
this.listenCalled = false;
|
|
@@ -247,20 +248,20 @@ class Application extends Router {
|
|
|
247
248
|
this._draining = false;
|
|
248
249
|
// read here, at construction, the way express does; an empty NODE_ENV means development,
|
|
249
250
|
// which the ?? in the shared default would miss
|
|
250
|
-
if (typeof this.
|
|
251
|
-
this.
|
|
251
|
+
if (typeof this._settings.env === "undefined") {
|
|
252
|
+
this._settings.env = process.env.NODE_ENV || "development";
|
|
252
253
|
}
|
|
253
254
|
for (const key in defaultSettings) {
|
|
254
|
-
if (typeof this.
|
|
255
|
+
if (typeof this._settings[key] === "undefined") {
|
|
255
256
|
if (typeof defaultSettings[key] === "function") {
|
|
256
|
-
this.
|
|
257
|
+
this._settings[key] = defaultSettings[key](this);
|
|
257
258
|
} else {
|
|
258
|
-
this.
|
|
259
|
+
this._settings[key] = defaultSettings[key];
|
|
259
260
|
}
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
// non-enumerable, so the marker never shows up walking the settings
|
|
263
|
-
Object.defineProperty(this.
|
|
264
|
+
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
264
265
|
configurable: true,
|
|
265
266
|
value: true
|
|
266
267
|
});
|
|
@@ -371,54 +372,60 @@ class Application extends Router {
|
|
|
371
372
|
if (!value) {
|
|
372
373
|
// compiled, not deleted: an explicit false must shadow a parent's setting when
|
|
373
374
|
// this app is mounted, and a deleted key would read straight through to it
|
|
374
|
-
this.
|
|
375
|
+
this._settings["trust proxy fn"] = compileTrust(false);
|
|
375
376
|
} else {
|
|
376
|
-
this.
|
|
377
|
+
this._settings["trust proxy fn"] = compileTrust(value);
|
|
377
378
|
}
|
|
378
379
|
// set explicitly, so a mount no longer inherits the parent's
|
|
379
|
-
Object.defineProperty(this.
|
|
380
|
+
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
380
381
|
configurable: true,
|
|
381
382
|
value: false
|
|
382
383
|
});
|
|
383
384
|
} else if (key === "stat cache") {
|
|
384
385
|
// compiled here so the read path is a number and not a duration to parse per request
|
|
385
|
-
this.
|
|
386
|
+
this._settings["stat cache ms"] = durationSetting(value, "stat cache");
|
|
386
387
|
} else if (key === "query parser") {
|
|
387
388
|
if (value === "extended") {
|
|
388
|
-
this.
|
|
389
|
+
this._settings["query parser fn"] = fastQueryParse;
|
|
389
390
|
} else if (value === "simple" || value === true) {
|
|
390
|
-
this.
|
|
391
|
+
this._settings["query parser fn"] = parseQuery;
|
|
391
392
|
} else if (typeof value === "function") {
|
|
392
|
-
this.
|
|
393
|
+
this._settings["query parser fn"] = value;
|
|
393
394
|
} else if (value === false) {
|
|
394
|
-
this.
|
|
395
|
+
this._settings["query parser fn"] = undefined;
|
|
395
396
|
} else {
|
|
396
397
|
// express's wording, which applications match on
|
|
397
398
|
throw new TypeError("unknown value for query parser function: " + value);
|
|
398
399
|
}
|
|
399
|
-
} else if (key === "etag") {
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
this._skipPresets.clear();
|
|
400
|
+
} else if (key === "etag methods") {
|
|
401
|
+
// fulmine's own: the methods whose send() computes a generated ETag. Unset means all
|
|
402
|
+
// of them, which is express's behaviour and what its suite asserts per method; naming
|
|
403
|
+
// ["GET", "HEAD"] skips the digest everywhere a validator can never match, which
|
|
404
|
+
// measured +21% on a 4KB POST answer. See issue #10.
|
|
405
|
+
if (value != null && (!Array.isArray(value) || value.some((m) => typeof m !== "string"))) {
|
|
406
|
+
throw new TypeError('"etag methods" wants an array of method names, or null for all of them');
|
|
408
407
|
}
|
|
408
|
+
value = value == null ? undefined : value.map((m) => m.toUpperCase());
|
|
409
|
+
} else if (key === "etag") {
|
|
410
|
+
// The skips are not taken back here. They used to be, because send consults freshness
|
|
411
|
+
// and the skip branch looked like it had not copied the headers for it, but that
|
|
412
|
+
// branch reads if-none-match, if-modified-since and cache-control by name whatever
|
|
413
|
+
// this setting says, see request.js:527, and req.fresh reads nothing else off the
|
|
414
|
+
// request. Registering a route or a middleware after listen still takes them back,
|
|
415
|
+
// see router.js:1615: that is a different question, about code the analysis never saw.
|
|
409
416
|
if (typeof value === "function") {
|
|
410
|
-
this.
|
|
417
|
+
this._settings["etag fn"] = value;
|
|
411
418
|
} else {
|
|
412
419
|
switch (value) {
|
|
413
420
|
case true:
|
|
414
421
|
case "weak":
|
|
415
|
-
this.
|
|
422
|
+
this._settings["etag fn"] = createETagGenerator({ weak: true });
|
|
416
423
|
break;
|
|
417
424
|
case "strong":
|
|
418
|
-
this.
|
|
425
|
+
this._settings["etag fn"] = createETagGenerator({ weak: false });
|
|
419
426
|
break;
|
|
420
427
|
case false:
|
|
421
|
-
delete this.
|
|
428
|
+
delete this._settings["etag fn"];
|
|
422
429
|
break;
|
|
423
430
|
default:
|
|
424
431
|
// express's wording, which applications match on
|
|
@@ -427,7 +434,9 @@ class Application extends Router {
|
|
|
427
434
|
}
|
|
428
435
|
}
|
|
429
436
|
|
|
430
|
-
this.
|
|
437
|
+
this._settings[key] = value;
|
|
438
|
+
// any app's hot-settings copy may resolve through this one, see Router#_hot
|
|
439
|
+
settingsEpoch.n++;
|
|
431
440
|
return this;
|
|
432
441
|
}
|
|
433
442
|
|
|
@@ -520,13 +529,21 @@ class Application extends Router {
|
|
|
520
529
|
_serveGeneric(res, req) {
|
|
521
530
|
const request = this.handleRequest(res, req);
|
|
522
531
|
const response = request.res;
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
532
|
+
if (request._badFraming === true) {
|
|
533
|
+
return this._refuseFraming(response);
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
this._routeRequestDirect(request, response);
|
|
537
|
+
} finally {
|
|
538
|
+
// the synchronous stretch has run under the cork uWS holds for this callback, and
|
|
539
|
+
// whatever comes after it is outside
|
|
540
|
+
response._corkNeeded = true;
|
|
541
|
+
// an abort can only arrive after this callback returns, as the native handler's
|
|
542
|
+
// finally says: a response that finished inside it never needs uWS told at all
|
|
543
|
+
if (!response.finished) {
|
|
544
|
+
this._armAbort(res, response);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
530
547
|
}
|
|
531
548
|
|
|
532
549
|
/**
|
package/src/cli.js
CHANGED
|
@@ -85,10 +85,11 @@ const DIFFERENCES = [
|
|
|
85
85
|
'Express sends X-Powered-By: Express unless told not to. Set app.set("x-powered-by", true) to send it.'
|
|
86
86
|
],
|
|
87
87
|
[
|
|
88
|
-
"a compiled route is framed differently and
|
|
89
|
-
"A handler simple enough to be read at registration time is answered natively
|
|
90
|
-
"
|
|
91
|
-
|
|
88
|
+
"a compiled route is framed differently and keeps its connection header",
|
|
89
|
+
"A handler simple enough to be read at registration time is answered natively: chunked framing\n" +
|
|
90
|
+
"with no Content-Length, and a client that sent Connection: close is still told keep-alive,\n" +
|
|
91
|
+
"though the socket does close. A response that would carry a validator is never compiled, so\n" +
|
|
92
|
+
'conditional requests behave as on Express. app.set("declarative responses", false) turns it off.'
|
|
92
93
|
],
|
|
93
94
|
[
|
|
94
95
|
"headers are capped at 4096 bytes by default",
|
|
@@ -493,9 +494,43 @@ ${error.stack ?? error}`);
|
|
|
493
494
|
);
|
|
494
495
|
return null;
|
|
495
496
|
}
|
|
497
|
+
stopFileWorkers(apps);
|
|
496
498
|
return { apps, entry };
|
|
497
499
|
}
|
|
498
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Ends the file-reading threads that building an application started.
|
|
503
|
+
*
|
|
504
|
+
* An Application starts one per `threads` in its constructor, and these commands only ever read
|
|
505
|
+
* what compiling the routes decided: nothing here serves a file, so nothing here needs a thread.
|
|
506
|
+
* They are unref'd, so leaving them would not hang the process, but they are threads holding the
|
|
507
|
+
* library the application loaded, and this command is often not the whole process. It also stops
|
|
508
|
+
* them outliving the directory they were loaded from, which is how a test that profiles a copy and
|
|
509
|
+
* then removes it saw "Cannot find module .../src/worker.js" arrive after it had finished.
|
|
510
|
+
*
|
|
511
|
+
* Best effort throughout: a build with no workers, or a worker already gone, is not an error here.
|
|
512
|
+
*
|
|
513
|
+
* @param {any[]} apps
|
|
514
|
+
* @returns {void}
|
|
515
|
+
*/
|
|
516
|
+
function stopFileWorkers(apps) {
|
|
517
|
+
const seen = new Set();
|
|
518
|
+
for (const app of apps) {
|
|
519
|
+
for (const holder of app?.workers ?? []) {
|
|
520
|
+
const worker = holder?.worker;
|
|
521
|
+
if (!worker || seen.has(worker)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
seen.add(worker);
|
|
525
|
+
try {
|
|
526
|
+
worker.terminate();
|
|
527
|
+
} catch {
|
|
528
|
+
// a thread that never started, or already ended, needs nothing
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
499
534
|
/**
|
|
500
535
|
* Loads an application without letting it listen, and prints what compiling its routes decided.
|
|
501
536
|
*
|
package/src/declarative.js
CHANGED
|
@@ -50,6 +50,10 @@ const typeValueOf = (type) => (type.indexOf("/") === -1 ? contentTypeFor(type) :
|
|
|
50
50
|
const MAX_INSTRUCTION_LENGTH = 65535;
|
|
51
51
|
|
|
52
52
|
// the three that write a body, of which only one may appear
|
|
53
|
+
// The headers a conditional request is answered from. A compiled response cannot read the request,
|
|
54
|
+
// so it cannot honour one, and a handler that sets one has to stay on the ordinary path.
|
|
55
|
+
const VALIDATOR_HEADERS = new Set(["etag", "last-modified"]);
|
|
56
|
+
|
|
53
57
|
const bodyMethods = new Set(["send", "json", "end"]);
|
|
54
58
|
// and the four that finish the response, after which nothing a handler does is observable
|
|
55
59
|
const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
|
|
@@ -734,10 +738,13 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
734
738
|
}
|
|
735
739
|
|
|
736
740
|
for (const header of headers) {
|
|
737
|
-
|
|
741
|
+
const name = header[0].toLowerCase();
|
|
742
|
+
if (name === "content-length") {
|
|
738
743
|
return false;
|
|
739
744
|
}
|
|
740
|
-
|
|
745
|
+
// lowercased as the ordinary path stores every name, so the two paths answer the
|
|
746
|
+
// same bytes whatever casing the handler wrote, see issue #7
|
|
747
|
+
decRes = decRes.writeHeader(name, header[1]);
|
|
741
748
|
}
|
|
742
749
|
|
|
743
750
|
// sendStatus sends the status message as its body. It has to join `body` here, before the
|
|
@@ -748,24 +755,22 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
748
755
|
body.push({ type: "text", value: statuses.message[statusCode] || String(statusCode) });
|
|
749
756
|
}
|
|
750
757
|
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
}
|
|
758
|
+
// A response that would carry a validator is not compiled at all.
|
|
759
|
+
//
|
|
760
|
+
// µWS answers a declarative response without reading the request, so it cannot answer a
|
|
761
|
+
// conditional GET: it used to write an ETag computed over the compiled body at listen and
|
|
762
|
+
// then ignore it, so every revalidation got 200 and the whole body where Express answers
|
|
763
|
+
// 304 with none. Dropping the ETag instead would have kept the route compiled, at the
|
|
764
|
+
// price of no validator at all on the simplest routes of every application. Refusing
|
|
765
|
+
// keeps Express's answer, and `etag` false is how a route that does not need one stays
|
|
766
|
+
// compiled, which is what both benchmarks here already set.
|
|
767
|
+
if (headers.some((header) => VALIDATOR_HEADERS.has(header[0].toLowerCase()))) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
// an empty body gets no ETag, in Express and on the ordinary path here, so it has nothing
|
|
771
|
+
// to lose by being compiled
|
|
772
|
+
if (body.length && (bodyFromSend || sendStatusUsed) && app.get("etag")) {
|
|
773
|
+
return false;
|
|
769
774
|
}
|
|
770
775
|
|
|
771
776
|
// No Content-Length header here: uWS writes the framing itself, and a response carrying
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,15 @@ try {
|
|
|
35
35
|
// older uWS builds do not expose _cfg; there is nothing to fall back to
|
|
36
36
|
}
|
|
37
37
|
|
|
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
|
|
42
|
+
require("node:module").enableCompileCache?.();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// node below 22.8, or a disk the cache cannot be written to
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
// The factory doubles as a namespace, as in Express: Router, static and the body parsers hang off
|
|
39
48
|
// the function that creates an app.
|
|
40
49
|
//
|
package/src/middlewares.js
CHANGED
|
@@ -538,7 +538,7 @@ function serveStatic(root, options) {
|
|
|
538
538
|
filePath,
|
|
539
539
|
req.headers["accept-encoding"],
|
|
540
540
|
twinTtl,
|
|
541
|
-
req.app.
|
|
541
|
+
req.app._settings["stat cache ms"]
|
|
542
542
|
);
|
|
543
543
|
if (twin) {
|
|
544
544
|
stat = twin.stat;
|
|
@@ -546,7 +546,7 @@ function serveStatic(root, options) {
|
|
|
546
546
|
}
|
|
547
547
|
try {
|
|
548
548
|
if (stat === undefined) {
|
|
549
|
-
stat = cachedStat(statTarget, req.app.
|
|
549
|
+
stat = cachedStat(statTarget, req.app._settings["stat cache ms"]);
|
|
550
550
|
}
|
|
551
551
|
} catch (err) {
|
|
552
552
|
// the one to report when nothing is found: send hands each failed attempt to the next
|
|
@@ -656,7 +656,12 @@ function serveStatic(root, options) {
|
|
|
656
656
|
// already found before the stat below, on the ordinary path
|
|
657
657
|
const variant =
|
|
658
658
|
twin ??
|
|
659
|
-
pickPrecompressed(
|
|
659
|
+
pickPrecompressed(
|
|
660
|
+
filePath,
|
|
661
|
+
req.headers["accept-encoding"],
|
|
662
|
+
twinTtl,
|
|
663
|
+
req.app._settings["stat cache ms"]
|
|
664
|
+
);
|
|
660
665
|
if (variant) {
|
|
661
666
|
_path += variant.suffix;
|
|
662
667
|
stat = variant.stat;
|
|
@@ -935,13 +940,16 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
935
940
|
// upstream middleware's AsyncLocalStorage must still be there when next runs
|
|
936
941
|
next = AsyncResource.bind(next);
|
|
937
942
|
|
|
938
|
-
// with
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
+
// with nothing to decompress, uWS can collect the whole body in native code: one
|
|
944
|
+
// callback instead of one per chunk, the limit enforced before any byte reaches JS,
|
|
945
|
+
// and no copy at all - the parsers turn the bytes into req.body before the callback
|
|
946
|
+
// returns, so a view over uWS's own memory is enough. A declared length was the
|
|
947
|
+
// original case; a chunked body accumulates in the same native vector and only loses
|
|
948
|
+
// the length check, since there is no declaration to hold it to
|
|
949
|
+
const declared = Number(length);
|
|
950
|
+
const declaresLength = !isNaN(declared) && declared > 0;
|
|
951
|
+
if (!req.receivedData && !inflate && req._res.collectBody && (declaresLength || isNaN(declared))) {
|
|
943
952
|
req.bodyRead = true;
|
|
944
|
-
const declared = Number(length);
|
|
945
953
|
req._res.collectBody(limit, (body) => {
|
|
946
954
|
if (body === null) {
|
|
947
955
|
// over maxSize: uWS refused it natively
|
|
@@ -952,7 +960,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
952
960
|
})
|
|
953
961
|
);
|
|
954
962
|
}
|
|
955
|
-
if (body.byteLength !== declared) {
|
|
963
|
+
if (declaresLength && body.byteLength !== declared) {
|
|
956
964
|
return next(
|
|
957
965
|
bodyError("request size did not match content length", 400, "request.size.invalid", {
|
|
958
966
|
expected: declared,
|