fulmine.js 5.1.4 → 5.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +23 -2
- package/README.md +26 -1
- package/package.json +1 -1
- package/src/application.js +15 -4
- package/src/middlewares.js +98 -20
- package/src/request.js +71 -4
- package/src/router.js +104 -10
- package/src/usage.js +247 -0
package/NOTICE
CHANGED
|
@@ -12,6 +12,10 @@ itself and can be inspected with `git log` and `git shortlog -sn`.
|
|
|
12
12
|
Fulmine is not affiliated with, endorsed by, or maintained by the authors of
|
|
13
13
|
Ultimate Express.
|
|
14
14
|
|
|
15
|
+
This product includes code derived from fast-querystring
|
|
16
|
+
(https://github.com/anonrig/fast-querystring), Copyright (c) Yagiz Nizipli,
|
|
17
|
+
licensed under the MIT License, vendored in src/parse-query.js.
|
|
18
|
+
|
|
15
19
|
As required by section 4(b) of the Apache License, the following are the
|
|
16
20
|
significant changes made to the original work:
|
|
17
21
|
|
|
@@ -19,10 +23,27 @@ significant changes made to the original work:
|
|
|
19
23
|
- Public API documented, and type checked from those annotations.
|
|
20
24
|
- Router dispatch reworked so a chain of N middlewares allocates one promise
|
|
21
25
|
instead of N, and the mount stack is no longer walked on every hop.
|
|
22
|
-
- Request bodies are collected
|
|
23
|
-
|
|
26
|
+
- Request bodies are collected in native code by uWebSockets.js when the
|
|
27
|
+
length is known, with the size limit enforced before any byte reaches
|
|
28
|
+
JavaScript, instead of being copied once per chunk and again on
|
|
29
|
+
concatenation; the body parsers read the few headers they need from the
|
|
30
|
+
raw header entries and bind the caller's async context only when a read
|
|
31
|
+
actually goes asynchronous.
|
|
24
32
|
- Empty request bodies now produce the same value each Express body parser
|
|
25
33
|
produces, rather than being skipped.
|
|
34
|
+
- Routes whose path is a literal are registered as native uWebSockets.js
|
|
35
|
+
routes, with their path and method handed to the request as
|
|
36
|
+
registration-time constants.
|
|
37
|
+
- sendFile answers small unchanged files from a stat-validated cache, and
|
|
38
|
+
concurrent reads of the same file are coalesced.
|
|
39
|
+
- The query string parser is vendored from fast-querystring and answers on a
|
|
40
|
+
bare null prototype, matching how Express displays parsed queries.
|
|
41
|
+
- Express 5's own test suite runs against Fulmine as a CI gate, from a
|
|
42
|
+
pinned checkout of Express.
|
|
43
|
+
- The app is callable as a request listener, so http.createServer(app),
|
|
44
|
+
supertest and anything else that invokes an app directly keeps working
|
|
45
|
+
through a node:http shim.
|
|
46
|
+
- Releases are built and published to npm from CI.
|
|
26
47
|
- Benchmark harness reworked: wrk replaced by autocannon so the suite runs
|
|
27
48
|
anywhere Node does, NODE_ENV is set, load errors and response validation are
|
|
28
49
|
no longer discarded, rows bounded by shared work are labelled, and scenarios
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ to run it yourself.
|
|
|
55
55
|
|
|
56
56
|
Numbers produced by a project about itself deserve suspicion, so Fulmine also stands in public arenas, run by their own rigs under their own rules:
|
|
57
57
|
|
|
58
|
-
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): the saved run measures
|
|
58
|
+
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): first among the JavaScript entries and second overall across every language on the board. The saved run measures 7.64 million pipelined requests per second, 1.12 million on the json profile, 457 thousand on compressed json and 222 thousand on the Postgres profile.
|
|
59
59
|
- **[web-frameworks](https://github.com/the-benchmarker/web-frameworks)**: entry merged, numbers arrive with their next published round.
|
|
60
60
|
|
|
61
61
|
More to come as their maintainers take the entries in.
|
|
@@ -90,6 +90,31 @@ npx fulmine differences # print the list below and change nothing
|
|
|
90
90
|
The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
|
|
91
91
|
command whose name ends in `.js` on Windows, where it exits without a word.
|
|
92
92
|
|
|
93
|
+
## Docker
|
|
94
|
+
|
|
95
|
+
Two things about µWebSockets.js make a Dockerfile that works for Express fail here, and both have easy answers:
|
|
96
|
+
|
|
97
|
+
- **No Alpine, and no Debian bookworm either.** µWebSockets.js ships prebuilt binaries linked against glibc 2.38 or newer. Alpine images use musl, so the binary does not load at all; `node:22` and `node:22-slim` are Debian bookworm, whose glibc 2.36 fails at startup with `GLIBC_2.38' not found`. Use the trixie variants: `node:22-trixie-slim` and up.
|
|
98
|
+
- **`git` must be there when `npm install` runs.** µWebSockets.js is not on npm; it is installed straight from GitHub (`github:uNetworking/uWebSockets.js`), and npm uses git to fetch it. Full images like `node:22-trixie` have git; `-slim` ones do not.
|
|
99
|
+
|
|
100
|
+
The clean way to satisfy both is a multi-stage build: install with the full image, run with the slim one.
|
|
101
|
+
|
|
102
|
+
```dockerfile
|
|
103
|
+
FROM node:22-trixie AS build
|
|
104
|
+
WORKDIR /app
|
|
105
|
+
COPY package*.json ./
|
|
106
|
+
RUN npm ci --omit=dev
|
|
107
|
+
|
|
108
|
+
FROM node:22-trixie-slim
|
|
109
|
+
WORKDIR /app
|
|
110
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
111
|
+
COPY . .
|
|
112
|
+
EXPOSE 3000
|
|
113
|
+
CMD ["node", "server.js"]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A single-stage `node:22-trixie-slim` image works too if you `apt-get install -y git ca-certificates` before `npm ci`. Prebuilt binaries exist for x64 and arm64 on Linux, macOS and Windows, so nothing is compiled at install time either way.
|
|
117
|
+
|
|
93
118
|
## Differences from Express
|
|
94
119
|
|
|
95
120
|
- `app.listen()` returns the app, not an `http.Server`. There is no node server underneath, so `server.close()`, `server.address()` and anything that attaches itself to a real `http.Server` need a look. `app.close()`, `app.address()` and `app.listening` are there and do what you would expect.
|
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -128,9 +128,10 @@ class Application extends Router {
|
|
|
128
128
|
* @param {any} res
|
|
129
129
|
* @param {any} app
|
|
130
130
|
* @param {any} [preset]
|
|
131
|
+
* @param {any} [skipHolder]
|
|
131
132
|
*/
|
|
132
|
-
constructor(req, res, app, preset) {
|
|
133
|
-
super(req, res, app, preset);
|
|
133
|
+
constructor(req, res, app, preset, skipHolder) {
|
|
134
|
+
super(req, res, app, preset, skipHolder);
|
|
134
135
|
}
|
|
135
136
|
};
|
|
136
137
|
this._response = class extends Response {
|
|
@@ -359,6 +360,14 @@ class Application extends Router {
|
|
|
359
360
|
this.settings[key] = Array.isArray(value) ? value.map((dir) => path.resolve(dir)) : path.resolve(value);
|
|
360
361
|
return this;
|
|
361
362
|
} else if (key === "etag") {
|
|
363
|
+
// an etag arriving after listen would make send consult freshness headers the
|
|
364
|
+
// header-skip routes never copied, so those skips are taken back
|
|
365
|
+
if (value !== false && this._skipPresets?.size) {
|
|
366
|
+
for (const preset of this._skipPresets) {
|
|
367
|
+
preset.skipHeaders = false;
|
|
368
|
+
}
|
|
369
|
+
this._skipPresets.clear();
|
|
370
|
+
}
|
|
362
371
|
if (typeof value === "function") {
|
|
363
372
|
this.settings["etag fn"] = value;
|
|
364
373
|
} else {
|
|
@@ -431,10 +440,12 @@ class Application extends Router {
|
|
|
431
440
|
* @param {any} res uWS response
|
|
432
441
|
* @param {any} req uWS request, readable only during this call
|
|
433
442
|
* @param {any} [preset] a literal registration's constants, see nativePreset in the router
|
|
443
|
+
* @param {any} [skipHolder] where a granted header skip lives, forwarded whole: dropping
|
|
444
|
+
* it here silently turned every skip off, since the native closures call this override
|
|
434
445
|
* @returns {any} the request, with the response reachable as request.res
|
|
435
446
|
*/
|
|
436
|
-
handleRequest(res, req, preset) {
|
|
437
|
-
const request = super.handleRequest(res, req, preset);
|
|
447
|
+
handleRequest(res, req, preset, skipHolder) {
|
|
448
|
+
const request = super.handleRequest(res, req, preset, skipHolder);
|
|
438
449
|
// removal rides the close listener the Response constructor already has, since a second
|
|
439
450
|
// once() per request measured a tenth of a microsecond on the hot path.
|
|
440
451
|
// An aborted response only flips its flags without emitting 'close', which is why
|
package/src/middlewares.js
CHANGED
|
@@ -22,6 +22,7 @@ const zlib = require("fast-zlib");
|
|
|
22
22
|
const typeis = require("type-is");
|
|
23
23
|
const qs = require("qs");
|
|
24
24
|
const parseQuery = require("./parse-query.js");
|
|
25
|
+
const { kGetSafe } = require("./usage.js");
|
|
25
26
|
const { AsyncResource } = require("async_hooks");
|
|
26
27
|
const { fastQueryParse, NullObject, asStatError, httpError, memoizeByString } = require("./utils.js");
|
|
27
28
|
|
|
@@ -289,7 +290,10 @@ function serveStatic(root, options) {
|
|
|
289
290
|
options._ownEtag = true;
|
|
290
291
|
|
|
291
292
|
return (req, res, next) => {
|
|
292
|
-
|
|
293
|
+
// Not bound here: every path down to sendFile is synchronous, statSync included, so the
|
|
294
|
+
// caller's async context is intact at each of these next() calls. Only sendFile's
|
|
295
|
+
// completion can arrive on a uWS callback that carries no context, and that one
|
|
296
|
+
// continuation is bound where it is handed over.
|
|
293
297
|
|
|
294
298
|
// a file is read, not written: anything but GET and HEAD belongs to whoever comes next, or
|
|
295
299
|
// is refused outright when this middleware is the last word
|
|
@@ -410,11 +414,15 @@ function serveStatic(root, options) {
|
|
|
410
414
|
|
|
411
415
|
options._stat = stat;
|
|
412
416
|
|
|
413
|
-
return res.sendFile(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
417
|
+
return res.sendFile(
|
|
418
|
+
_path,
|
|
419
|
+
options,
|
|
420
|
+
AsyncResource.bind((e) => {
|
|
421
|
+
if (e) {
|
|
422
|
+
next(options.fallthrough && FALLTHROUGH_STATUSES.has(e.status) ? undefined : e);
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
);
|
|
418
426
|
};
|
|
419
427
|
}
|
|
420
428
|
|
|
@@ -462,9 +470,11 @@ function createInflate(contentEncoding) {
|
|
|
462
470
|
* @param {string} [charsetPolicy] which charsets this parser accepts, as body-parser draws the
|
|
463
471
|
* lines: "utf" (json, utf-* only), "urlencoded" (utf-8 and iso-8859-1), "any" (anything iconv
|
|
464
472
|
* knows), or undefined for a parser that never decodes (raw)
|
|
473
|
+
* @param {boolean} [keepsBuffer] whether the collected buffer itself escapes to the application,
|
|
474
|
+
* which rules out handing it a view over uWS memory
|
|
465
475
|
* @returns {(options?: object) => Function} the middleware factory
|
|
466
476
|
*/
|
|
467
|
-
function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy) {
|
|
477
|
+
function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy, keepsBuffer) {
|
|
468
478
|
return function (options) {
|
|
469
479
|
// a copy, because everything below writes the parsed values back: with the caller's own
|
|
470
480
|
// object, altering it after the parser was built would alter the parser
|
|
@@ -500,6 +510,10 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
500
510
|
}
|
|
501
511
|
if (typeof options.defaultCharset === "undefined") options.defaultCharset = "utf-8";
|
|
502
512
|
|
|
513
|
+
// whether the collected bytes escape the collection callback: the raw parser hands the
|
|
514
|
+
// buffer itself to the application, and a verify hook may keep what it is shown
|
|
515
|
+
const copyBody = keepsBuffer || typeof options.verify === "function";
|
|
516
|
+
|
|
503
517
|
// Whether a content-type is one this parser claims, remembered per parser.
|
|
504
518
|
//
|
|
505
519
|
// Only reached when the caller asked for a wildcard or a list, since a plain type takes the
|
|
@@ -513,15 +527,18 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
513
527
|
|
|
514
528
|
let additionalMethods;
|
|
515
529
|
|
|
516
|
-
|
|
517
|
-
|
|
530
|
+
const parserMiddleware = (req, res, next) => {
|
|
531
|
+
// Not bound yet: every return in this prologue is synchronous, so the caller's async
|
|
532
|
+
// context is still intact and an AsyncResource here would be 1.4 microseconds of
|
|
533
|
+
// nothing. The bind happens below, only once a real read is about to go async.
|
|
518
534
|
|
|
519
535
|
// skip reading body twice
|
|
520
536
|
if (req.bodyRead) {
|
|
521
537
|
return next();
|
|
522
538
|
}
|
|
523
539
|
|
|
524
|
-
|
|
540
|
+
// straight from the raw entries: three headers do not justify building the object
|
|
541
|
+
const type = req._rawHeader("content-type");
|
|
525
542
|
|
|
526
543
|
// req.body is deliberately left undefined until a parser claims the request. That is
|
|
527
544
|
// what lets a handler tell "nothing parsed this" apart from "the body was empty",
|
|
@@ -534,13 +551,13 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
534
551
|
return next();
|
|
535
552
|
}
|
|
536
553
|
|
|
537
|
-
const length = req.
|
|
554
|
+
const length = req._rawHeader("content-length");
|
|
538
555
|
|
|
539
556
|
// No content-length and no transfer-encoding means the request carries no body at all,
|
|
540
557
|
// and a body parser must leave it alone rather than parse nothing into an empty value.
|
|
541
558
|
// type-is applies this before matching the type, but the simpleType shortcut below
|
|
542
559
|
// compares strings directly and would otherwise skip the check.
|
|
543
|
-
if (req.
|
|
560
|
+
if (req._rawHeader("transfer-encoding") === undefined && isNaN(length)) {
|
|
544
561
|
return next();
|
|
545
562
|
}
|
|
546
563
|
|
|
@@ -620,7 +637,8 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
620
637
|
const abs = [];
|
|
621
638
|
let inflate;
|
|
622
639
|
let totalSize = 0;
|
|
623
|
-
const
|
|
640
|
+
const rawContentEncoding = req._rawHeader("content-encoding");
|
|
641
|
+
const contentEncoding = (rawContentEncoding || "identity").toLowerCase();
|
|
624
642
|
if (!options.inflate && contentEncoding !== "identity") {
|
|
625
643
|
return next(
|
|
626
644
|
bodyError("content encoding unsupported", 415, "encoding.unsupported", {
|
|
@@ -629,19 +647,64 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
629
647
|
);
|
|
630
648
|
}
|
|
631
649
|
if (options.inflate) {
|
|
632
|
-
inflate = createInflate(
|
|
650
|
+
inflate = createInflate(rawContentEncoding);
|
|
633
651
|
if (inflate === false) {
|
|
634
652
|
return next(
|
|
635
653
|
bodyError(
|
|
636
|
-
'unsupported content encoding "' +
|
|
654
|
+
'unsupported content encoding "' + rawContentEncoding + '"',
|
|
637
655
|
415,
|
|
638
656
|
"encoding.unsupported",
|
|
639
|
-
{
|
|
657
|
+
{
|
|
658
|
+
encoding: rawContentEncoding
|
|
659
|
+
}
|
|
640
660
|
)
|
|
641
661
|
);
|
|
642
662
|
}
|
|
643
663
|
}
|
|
644
664
|
|
|
665
|
+
// From here the body really gets read, and uWS delivers it on native callbacks that
|
|
666
|
+
// carry no async context, so this is the one continuation that has to be bound: an
|
|
667
|
+
// upstream middleware's AsyncLocalStorage must still be there when next runs
|
|
668
|
+
next = AsyncResource.bind(next);
|
|
669
|
+
|
|
670
|
+
// with a known content-length and nothing to decompress, uWS can collect the whole
|
|
671
|
+
// body in native code: one callback instead of one per chunk, the limit enforced
|
|
672
|
+
// before any byte reaches JS, and no copy at all - the parsers turn the bytes into
|
|
673
|
+
// req.body before the callback returns, so a view over uWS's own memory is enough
|
|
674
|
+
if (!req.receivedData && !inflate && !isNaN(length) && Number(length) > 0 && req._res.collectBody) {
|
|
675
|
+
req.bodyRead = true;
|
|
676
|
+
const declared = Number(length);
|
|
677
|
+
req._res.collectBody(options.limit, (body) => {
|
|
678
|
+
if (body === null) {
|
|
679
|
+
// over maxSize: uWS refused it natively
|
|
680
|
+
return next(
|
|
681
|
+
bodyError("request entity too large", 413, "entity.too.large", {
|
|
682
|
+
limit: options.limit,
|
|
683
|
+
received: options.limit
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
if (body.byteLength !== declared) {
|
|
688
|
+
return next(
|
|
689
|
+
bodyError("request size did not match content length", 400, "request.size.invalid", {
|
|
690
|
+
expected: declared,
|
|
691
|
+
length: declared,
|
|
692
|
+
received: body.byteLength
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
let buf = Buffer.from(body);
|
|
697
|
+
if (copyBody) {
|
|
698
|
+
buf = Buffer.from(buf);
|
|
699
|
+
}
|
|
700
|
+
if (!runVerify(req, res, next, options, buf)) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
beforeReturn(req, res, next, options, buf, encoding);
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
645
708
|
// uWS neuters its ArrayBuffer after the callback, so every chunk has to be copied out of
|
|
646
709
|
// it - and then Buffer.concat copied the whole body a second time. when content-length is
|
|
647
710
|
// known and we aren't inflating, the final size is known up front, so chunks can go
|
|
@@ -813,6 +876,14 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
813
876
|
req.on("end", onEnd);
|
|
814
877
|
}
|
|
815
878
|
};
|
|
879
|
+
// A GET without a declared body leaves this middleware through the synchronous
|
|
880
|
+
// no-body exit before anything type- or charset-shaped is read, and a GET that does
|
|
881
|
+
// declare one takes the full header copy in the constructor, so the header-skip
|
|
882
|
+
// analysis may trust it. A type function sees the request itself, so it may not.
|
|
883
|
+
if (typeof options.type !== "function") {
|
|
884
|
+
parserMiddleware[kGetSafe] = true;
|
|
885
|
+
}
|
|
886
|
+
return parserMiddleware;
|
|
816
887
|
};
|
|
817
888
|
}
|
|
818
889
|
|
|
@@ -860,10 +931,17 @@ const json = createBodyParser(
|
|
|
860
931
|
"utf"
|
|
861
932
|
);
|
|
862
933
|
|
|
863
|
-
const raw = createBodyParser(
|
|
864
|
-
|
|
865
|
-
next
|
|
866
|
-
|
|
934
|
+
const raw = createBodyParser(
|
|
935
|
+
"application/octet-stream",
|
|
936
|
+
function (req, res, next, options, buf) {
|
|
937
|
+
req.body = buf;
|
|
938
|
+
next();
|
|
939
|
+
},
|
|
940
|
+
undefined,
|
|
941
|
+
undefined,
|
|
942
|
+
// req.body is the collected buffer itself, so it must not be a view over uWS memory
|
|
943
|
+
true
|
|
944
|
+
);
|
|
867
945
|
|
|
868
946
|
const text = createBodyParser(
|
|
869
947
|
"text/plain",
|
package/src/request.js
CHANGED
|
@@ -200,16 +200,61 @@ module.exports = class Request extends Readable {
|
|
|
200
200
|
* @param {any} [preset] a literal native registration's constants: µWS matched the URL byte
|
|
201
201
|
* for byte against that exact pattern and dispatched by method, so path, method and what
|
|
202
202
|
* derives from them are known without asking
|
|
203
|
+
* @param {any} [skipHolder] where a granted header skip lives: the preset itself for a
|
|
204
|
+
* literal registration, a holder of its own for a parameterised one
|
|
203
205
|
*/
|
|
204
|
-
constructor(req, res, app, preset) {
|
|
206
|
+
constructor(req, res, app, preset, skipHolder) {
|
|
205
207
|
// the same object every time: Readable reads these options and never writes to them
|
|
206
208
|
super(READABLE_OPTIONS);
|
|
207
209
|
this._res = res;
|
|
208
210
|
this._req = req;
|
|
209
211
|
this.readable = true;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
if (skipHolder !== undefined && skipHolder.skipHeaders) {
|
|
213
|
+
// The chain behind this registration provably never reads a header, so instead of
|
|
214
|
+
// copying them all out of uWS the constructor asks for the four that steer the
|
|
215
|
+
// framework itself: body framing, keep-alive, and accept for the error page a
|
|
216
|
+
// throw could still need. A GET that does declare a body is the rare case, and
|
|
217
|
+
// the parsers and the stream want the whole picture, so it takes the full copy.
|
|
218
|
+
const length = req.getHeader("content-length");
|
|
219
|
+
const transferEncoding = req.getHeader("transfer-encoding");
|
|
220
|
+
if ((length !== "" && length !== "0") || transferEncoding !== "") {
|
|
221
|
+
currentRequest = this;
|
|
222
|
+
this._req.forEach(Request.#collectHeader);
|
|
223
|
+
currentRequest = null;
|
|
224
|
+
} else {
|
|
225
|
+
const entries = this.#rawHeadersEntries;
|
|
226
|
+
const connection = req.getHeader("connection");
|
|
227
|
+
if (connection !== "") {
|
|
228
|
+
entries.push("connection", connection);
|
|
229
|
+
if (connection.length === 5 && connection.toLowerCase() === "close") {
|
|
230
|
+
this._connectionClose = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const accept = req.getHeader("accept");
|
|
234
|
+
if (accept !== "") {
|
|
235
|
+
entries.push("accept", accept);
|
|
236
|
+
}
|
|
237
|
+
// send consults freshness whatever the etag setting: if-none-match can be "*"
|
|
238
|
+
// and a handler may set a validator by hand, so the conditional trio has to be
|
|
239
|
+
// really absent rather than merely uncopied
|
|
240
|
+
const ifNoneMatch = req.getHeader("if-none-match");
|
|
241
|
+
if (ifNoneMatch !== "") {
|
|
242
|
+
entries.push("if-none-match", ifNoneMatch);
|
|
243
|
+
}
|
|
244
|
+
const ifModifiedSince = req.getHeader("if-modified-since");
|
|
245
|
+
if (ifModifiedSince !== "") {
|
|
246
|
+
entries.push("if-modified-since", ifModifiedSince);
|
|
247
|
+
}
|
|
248
|
+
const cacheControl = req.getHeader("cache-control");
|
|
249
|
+
if (cacheControl !== "") {
|
|
250
|
+
entries.push("cache-control", cacheControl);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
currentRequest = this;
|
|
255
|
+
this._req.forEach(Request.#collectHeader);
|
|
256
|
+
currentRequest = null;
|
|
257
|
+
}
|
|
213
258
|
this.routeCount = 1;
|
|
214
259
|
this.key = key++;
|
|
215
260
|
if (key > 100000) {
|
|
@@ -324,6 +369,28 @@ module.exports = class Request extends Readable {
|
|
|
324
369
|
});
|
|
325
370
|
}
|
|
326
371
|
|
|
372
|
+
/**
|
|
373
|
+
* One header by its lowercase wire name, straight from the raw entries. The body parsers ask
|
|
374
|
+
* for three of these per request, and materializing the whole headers object for that costs
|
|
375
|
+
* more than all three scans together. Reads the built object instead when it already exists,
|
|
376
|
+
* so joined duplicates come out the same either way.
|
|
377
|
+
*
|
|
378
|
+
* @param {string} name lowercase
|
|
379
|
+
* @returns {string|undefined}
|
|
380
|
+
*/
|
|
381
|
+
_rawHeader(name) {
|
|
382
|
+
if (this.#cachedHeaders !== null) {
|
|
383
|
+
return this.#cachedHeaders[name];
|
|
384
|
+
}
|
|
385
|
+
const entries = this.#rawHeadersEntries;
|
|
386
|
+
for (let i = 0, len = entries.length; i < len; i += 2) {
|
|
387
|
+
if (entries[i] === name) {
|
|
388
|
+
return entries[i + 1];
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
327
394
|
/**
|
|
328
395
|
* Whether there is any point still reading the body: once the response is finished or the
|
|
329
396
|
* connection is gone, uWS has nothing left to hand over.
|
package/src/router.js
CHANGED
|
@@ -36,6 +36,7 @@ const compileDeclarative = require("./declarative.js");
|
|
|
36
36
|
const statuses = require("statuses");
|
|
37
37
|
const { METHODS } = require("http");
|
|
38
38
|
const { isNodeRequest, serveNodeRequest } = require("./node-shim.js");
|
|
39
|
+
const { chainSkipsHeaders } = require("./usage.js");
|
|
39
40
|
|
|
40
41
|
// every method the declarative compiler can emit: a patched one must disable compilation, or the
|
|
41
42
|
// patch would be honoured everywhere but on compiled routes
|
|
@@ -691,10 +692,38 @@ function nativePreset(path, method, strict) {
|
|
|
691
692
|
endsWithSlash,
|
|
692
693
|
opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
|
|
693
694
|
isOptions: method === "OPTIONS",
|
|
694
|
-
isHead: method === "HEAD"
|
|
695
|
+
isHead: method === "HEAD",
|
|
696
|
+
// set at registration when the whole chain provably never reads a header; mutable,
|
|
697
|
+
// because a middleware added after listen has to be able to take it back
|
|
698
|
+
skipHeaders: false
|
|
695
699
|
};
|
|
696
700
|
}
|
|
697
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Whether any error middleware exists anywhere under this router, mounted routers and sub-apps
|
|
704
|
+
* included. The header-skip analysis needs the answer to be no: a throw inside an analyzed
|
|
705
|
+
* handler would hand the request to code nobody analyzed.
|
|
706
|
+
*
|
|
707
|
+
* @param {any} router
|
|
708
|
+
* @returns {boolean}
|
|
709
|
+
*/
|
|
710
|
+
function hasErrorMiddleware(router) {
|
|
711
|
+
for (const route of router._routes) {
|
|
712
|
+
for (const callback of route.callbacks) {
|
|
713
|
+
// a mounted router or a callable sub-app carries routes of its own; the callable
|
|
714
|
+
// app is also a function, so the routes are looked for first
|
|
715
|
+
if (callback && callback._routes) {
|
|
716
|
+
if (hasErrorMiddleware(callback)) {
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
} else if (typeof callback === "function" && callback.length >= 4) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
|
|
698
727
|
/**
|
|
699
728
|
*
|
|
700
729
|
*/
|
|
@@ -822,6 +851,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
822
851
|
this._paramCallbacks = new Map();
|
|
823
852
|
this._mountpathCache = new Map();
|
|
824
853
|
this._routes = [];
|
|
854
|
+
// the native presets allowed to skip the header copy, so a late middleware or an etag
|
|
855
|
+
// arriving after listen can take the permission back; null until one is granted
|
|
856
|
+
/** @type {Set<any>|null} */
|
|
857
|
+
this._skipPresets = null;
|
|
858
|
+
/** @type {boolean|undefined} */
|
|
859
|
+
this._hasErrMwCache = undefined;
|
|
825
860
|
// an array when mounted on several paths at once, as Express allows
|
|
826
861
|
/** @type {string|string[]} */
|
|
827
862
|
this.mountpath = "/";
|
|
@@ -1107,6 +1142,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1107
1142
|
}
|
|
1108
1143
|
this._routes.push(...routes);
|
|
1109
1144
|
|
|
1145
|
+
// anything registered after listen invalidates what the header-skip analysis proved:
|
|
1146
|
+
// it could catch a throw or read what a chain never did, so every skip is taken back
|
|
1147
|
+
this._hasErrMwCache = undefined;
|
|
1148
|
+
if (this._skipPresets?.size) {
|
|
1149
|
+
for (const preset of this._skipPresets) {
|
|
1150
|
+
preset.skipHeaders = false;
|
|
1151
|
+
}
|
|
1152
|
+
this._skipPresets.clear();
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1110
1155
|
return parent;
|
|
1111
1156
|
}
|
|
1112
1157
|
|
|
@@ -1338,10 +1383,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
1338
1383
|
* @param {any} res uWS response
|
|
1339
1384
|
* @param {any} req uWS request, readable only during this call
|
|
1340
1385
|
* @param {any} [preset] a literal registration's constants, see nativePreset
|
|
1386
|
+
* @param {any} [skipHolder] the object a granted header skip lives on: the preset itself
|
|
1387
|
+
* for a literal registration, a holder of its own for a parameterised one
|
|
1341
1388
|
* @returns {any} the request, with the response reachable as request.res
|
|
1342
1389
|
*/
|
|
1343
|
-
handleRequest(res, req, preset) {
|
|
1344
|
-
const request = new this._request(req, res, this, preset);
|
|
1390
|
+
handleRequest(res, req, preset, skipHolder) {
|
|
1391
|
+
const request = new this._request(req, res, this, preset, skipHolder);
|
|
1345
1392
|
const response = new this._response(res, request, this);
|
|
1346
1393
|
request.res = response;
|
|
1347
1394
|
response.req = request;
|
|
@@ -1417,7 +1464,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1417
1464
|
if (route.path.includes(":")) {
|
|
1418
1465
|
route.optimizedParams = route.path.match(regExParam).map((p) => p.slice(1));
|
|
1419
1466
|
}
|
|
1420
|
-
const makeHandler = (chain, preset) => {
|
|
1467
|
+
const makeHandler = (chain, preset, skips) => {
|
|
1468
|
+
// the mutable object a granted skip lives on, so a middleware arriving after
|
|
1469
|
+
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
1470
|
+
// parameterised one, which has no preset, gets a holder of its own
|
|
1471
|
+
let skipHolder = preset;
|
|
1472
|
+
if (skipHolder === undefined && skips) {
|
|
1473
|
+
skipHolder = { skipHeaders: true };
|
|
1474
|
+
(this._skipPresets ??= new Set()).add(skipHolder);
|
|
1475
|
+
}
|
|
1421
1476
|
// all three are registration-time constants: computing them in the handler was a
|
|
1422
1477
|
// closure and a scan of the chain on every native request.
|
|
1423
1478
|
// Falling back resumes after the mount, not after the router's leaf: the leaf can have
|
|
@@ -1430,7 +1485,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1430
1485
|
// and this one never did. nativeDone and nativeFail defer their epilogues to a
|
|
1431
1486
|
// microtask, which is where the await used to resume, so the visible order holds
|
|
1432
1487
|
return (res, req) => {
|
|
1433
|
-
const request = this.handleRequest(res, req, preset);
|
|
1488
|
+
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
1434
1489
|
const response = request.res;
|
|
1435
1490
|
if (optimizedParams) {
|
|
1436
1491
|
request.optimizedParams = new NullObject();
|
|
@@ -1461,6 +1516,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1461
1516
|
const getChain =
|
|
1462
1517
|
route.method === "GET" ? optimizedPath.filter((r) => r.all || r.method !== "HEAD") : optimizedPath;
|
|
1463
1518
|
route.optimizedPath = optimizedPath;
|
|
1519
|
+
const headChain = getChain.length === optimizedPath.length ? getChain : optimizedPath;
|
|
1464
1520
|
|
|
1465
1521
|
// A fully literal registration knows path and method here, so each registration site
|
|
1466
1522
|
// hands the request constructor its own constants. An "any" registration serves every
|
|
@@ -1471,11 +1527,45 @@ module.exports = class Router extends EventEmitter {
|
|
|
1471
1527
|
// registering that path here is the only way it could
|
|
1472
1528
|
const strictHere = Boolean((route.owner ?? this).get("strict routing"));
|
|
1473
1529
|
|
|
1474
|
-
|
|
1530
|
+
// Whether requests served by this registration may skip the header copy: GET and its
|
|
1531
|
+
// HEAD twins only, the app must not compute etags (send would consult freshness
|
|
1532
|
+
// headers), no error middleware may exist anywhere (a throw hands the request to code
|
|
1533
|
+
// the analysis never saw), and every callback in the chain has to pass the source
|
|
1534
|
+
// analysis in usage.js, whose default answer is no.
|
|
1535
|
+
let getSkips = false;
|
|
1536
|
+
let headSkips = false;
|
|
1537
|
+
if (route.method === "GET" && this.get("etag") === false) {
|
|
1538
|
+
let hasErr = this._hasErrMwCache;
|
|
1539
|
+
if (hasErr === undefined) {
|
|
1540
|
+
hasErr = this._hasErrMwCache = hasErrorMiddleware(this);
|
|
1541
|
+
}
|
|
1542
|
+
if (!hasErr) {
|
|
1543
|
+
// a terminal next() may only fall into the framework's own 404, so no later
|
|
1544
|
+
// route may be able to catch the same path
|
|
1545
|
+
const owner = route.owner ?? this;
|
|
1546
|
+
const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
|
|
1547
|
+
getSkips = chainSkipsHeaders(getChain, noLaterMatch);
|
|
1548
|
+
headSkips = headChain === getChain ? getSkips : chainSkipsHeaders(headChain, noLaterMatch);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
// remembered so a middleware or setting arriving after listen can take the skips back
|
|
1552
|
+
const makePreset = (path, method, skips) => {
|
|
1553
|
+
const preset = nativePreset(path, method, strictHere);
|
|
1554
|
+
if (skips) {
|
|
1555
|
+
preset.skipHeaders = true;
|
|
1556
|
+
(this._skipPresets ??= new Set()).add(preset);
|
|
1557
|
+
}
|
|
1558
|
+
return preset;
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
let fn = makeHandler(
|
|
1562
|
+
getChain,
|
|
1563
|
+
canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
|
|
1564
|
+
getSkips
|
|
1565
|
+
);
|
|
1475
1566
|
const jsFn = fn;
|
|
1476
1567
|
|
|
1477
1568
|
let replacedPath = route.path;
|
|
1478
|
-
const headChain = getChain.length === optimizedPath.length ? getChain : optimizedPath;
|
|
1479
1569
|
|
|
1480
1570
|
// the response prototype the route will really run under: its own app's, which sees a
|
|
1481
1571
|
// method patched there or inherited from a parent app, falling back to the registering app
|
|
@@ -1505,13 +1595,17 @@ module.exports = class Router extends EventEmitter {
|
|
|
1505
1595
|
fn !== jsFn
|
|
1506
1596
|
? fn
|
|
1507
1597
|
: canPreset
|
|
1508
|
-
? makeHandler(getChain,
|
|
1598
|
+
? makeHandler(getChain, makePreset(route.path + "/", route.method, getSkips), getSkips)
|
|
1509
1599
|
: fn;
|
|
1510
1600
|
this.uwsApp[method](replacedPath + "/", slashFn);
|
|
1511
1601
|
if (method === "get") {
|
|
1512
1602
|
this.uwsApp.head(
|
|
1513
1603
|
replacedPath + "/",
|
|
1514
|
-
makeHandler(
|
|
1604
|
+
makeHandler(
|
|
1605
|
+
headChain,
|
|
1606
|
+
canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
|
|
1607
|
+
headSkips
|
|
1608
|
+
)
|
|
1515
1609
|
);
|
|
1516
1610
|
}
|
|
1517
1611
|
}
|
|
@@ -1519,7 +1613,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1519
1613
|
// its own handler always: the shared one would carry the GET registration's method
|
|
1520
1614
|
this.uwsApp.head(
|
|
1521
1615
|
replacedPath,
|
|
1522
|
-
makeHandler(headChain, canPreset ?
|
|
1616
|
+
makeHandler(headChain, canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined, headSkips)
|
|
1523
1617
|
);
|
|
1524
1618
|
}
|
|
1525
1619
|
}
|
package/src/usage.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const acorn = require("acorn");
|
|
4
|
+
|
|
5
|
+
// Marks a middleware the analysis may trust on a GET request without reading its source: the
|
|
6
|
+
// body parsers set it, whose prologue only reads body-framing headers and leaves a bodyless
|
|
7
|
+
// GET alone (and a GET that declares a body falls back to the full header copy).
|
|
8
|
+
const kGetSafe = Symbol("fulmine.getSafe");
|
|
9
|
+
|
|
10
|
+
// What a handler may do with `req` and still let the header copy be skipped: members whose
|
|
11
|
+
// reads never reach a header. Anything else, computed access included, keeps the copy.
|
|
12
|
+
// req.res and req.app are deliberately absent: the walk judges only the member directly on
|
|
13
|
+
// the parameter, so anything that can reach another object could reach headers through it.
|
|
14
|
+
const REQ_OK = new Set(["query", "params", "body", "method", "path", "url", "baseUrl", "originalUrl", "route"]);
|
|
15
|
+
|
|
16
|
+
// What a handler may do with `res`: writing the response. Anything that negotiates against
|
|
17
|
+
// request headers (format, redirect, sendFile, jsonp) is deliberately absent.
|
|
18
|
+
const RES_OK = new Set([
|
|
19
|
+
"json",
|
|
20
|
+
"send",
|
|
21
|
+
"end",
|
|
22
|
+
"status",
|
|
23
|
+
"sendStatus",
|
|
24
|
+
"set",
|
|
25
|
+
"setHeader",
|
|
26
|
+
"header",
|
|
27
|
+
"get",
|
|
28
|
+
"type",
|
|
29
|
+
"contentType",
|
|
30
|
+
"append",
|
|
31
|
+
"vary",
|
|
32
|
+
"links",
|
|
33
|
+
"locals",
|
|
34
|
+
"statusCode",
|
|
35
|
+
"writeHead",
|
|
36
|
+
"headersSent",
|
|
37
|
+
"finished",
|
|
38
|
+
"cork"
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// what the analysis can say about one callback
|
|
42
|
+
const NO = 0; // could read headers, or could not be read at all
|
|
43
|
+
const SAFE = 1; // never reads a header, never touches next
|
|
44
|
+
const SAFE_NEXT = 2; // never reads a header, calls next: fine mid-chain, and at the end of
|
|
45
|
+
// the chain only when no later route could catch the fall-through
|
|
46
|
+
|
|
47
|
+
const verdicts = new WeakMap();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* What one callback provably does. The default is NO: any shape this walk does not understand
|
|
51
|
+
* and any alias of req, res or next keeps the header copy. That inversion is what makes
|
|
52
|
+
* source analysis sound to act on.
|
|
53
|
+
*
|
|
54
|
+
* @param {Function} fn
|
|
55
|
+
* @returns {number} NO, SAFE or SAFE_NEXT
|
|
56
|
+
*/
|
|
57
|
+
function callbackSkipsHeaders(fn) {
|
|
58
|
+
if (fn[kGetSafe]) {
|
|
59
|
+
return SAFE_NEXT;
|
|
60
|
+
}
|
|
61
|
+
let verdict = verdicts.get(fn);
|
|
62
|
+
if (verdict === undefined) {
|
|
63
|
+
verdict = analyze(fn);
|
|
64
|
+
verdicts.set(fn, verdict);
|
|
65
|
+
}
|
|
66
|
+
return verdict;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** @param {Function} fn @returns {number} */
|
|
70
|
+
function analyze(fn) {
|
|
71
|
+
let code = fn.toString();
|
|
72
|
+
if (code.startsWith("function") || code.startsWith("async function")) {
|
|
73
|
+
code = code.replace(/function *\(/, "function __cb(");
|
|
74
|
+
}
|
|
75
|
+
let tree;
|
|
76
|
+
try {
|
|
77
|
+
tree = acorn.parse(code, { ecmaVersion: "latest" });
|
|
78
|
+
} catch {
|
|
79
|
+
// class methods and native functions do not parse alone, and unread code is unknown code
|
|
80
|
+
return NO;
|
|
81
|
+
}
|
|
82
|
+
let root = /** @type {any} */ (tree.body[0]);
|
|
83
|
+
if (!root) {
|
|
84
|
+
return NO;
|
|
85
|
+
}
|
|
86
|
+
if (root.type === "ExpressionStatement") {
|
|
87
|
+
root = root.expression;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
root.type !== "FunctionDeclaration" &&
|
|
91
|
+
root.type !== "ArrowFunctionExpression" &&
|
|
92
|
+
root.type !== "FunctionExpression"
|
|
93
|
+
) {
|
|
94
|
+
return NO;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const params = /** @type {any[]} */ (root.params);
|
|
98
|
+
// rest or destructured parameters alias the objects somewhere the walk cannot follow
|
|
99
|
+
for (const p of params) {
|
|
100
|
+
if (p.type !== "Identifier") {
|
|
101
|
+
return NO;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const reqName = params[0] ? params[0].name : null;
|
|
105
|
+
const resName = params[1] ? params[1].name : null;
|
|
106
|
+
const nextName = params[2] ? params[2].name : null;
|
|
107
|
+
|
|
108
|
+
// Every appearance of the three names in the whole body is judged, nested functions
|
|
109
|
+
// included: an inner binding that shadows one of them only makes this stricter, never
|
|
110
|
+
// looser, so scope tracking is not needed for soundness.
|
|
111
|
+
let ok = true;
|
|
112
|
+
let usesNext = false;
|
|
113
|
+
walk(root.body, null, (node, parent) => {
|
|
114
|
+
if (!ok || node.type !== "Identifier") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const name = node.name;
|
|
118
|
+
if (name === "eval" || name === "arguments") {
|
|
119
|
+
ok = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (name !== reqName && name !== resName && name !== nextName) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// being renamed inside a member expression (req.query's `query`) is not a use
|
|
126
|
+
if (parent && parent.type === "MemberExpression" && parent.property === node && !parent.computed) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// a redeclaration as an inner parameter or variable name is not a use either
|
|
130
|
+
if (
|
|
131
|
+
parent &&
|
|
132
|
+
(((parent.type === "FunctionDeclaration" ||
|
|
133
|
+
parent.type === "FunctionExpression" ||
|
|
134
|
+
parent.type === "ArrowFunctionExpression") &&
|
|
135
|
+
parent.params.includes(node)) ||
|
|
136
|
+
(parent.type === "VariableDeclarator" && parent.id === node) ||
|
|
137
|
+
(parent.type === "Property" && parent.key === node && !parent.computed))
|
|
138
|
+
) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (name === nextName) {
|
|
142
|
+
// calling next is how a chain advances, and past its end or with an error the
|
|
143
|
+
// request lands in the framework's own final handler, which the constructor's
|
|
144
|
+
// accept pre-read covers. Anything but a direct call aliases the continuation,
|
|
145
|
+
// and an argument that could be the string "route" would leave the chain for
|
|
146
|
+
// routes nobody analyzed, so only shapes that cannot be a string pass.
|
|
147
|
+
if (!parent || parent.type !== "CallExpression" || parent.callee !== node) {
|
|
148
|
+
ok = false;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
usesNext = true;
|
|
152
|
+
const args = parent.arguments;
|
|
153
|
+
if (args.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const arg = args[0];
|
|
157
|
+
if (
|
|
158
|
+
args.length > 1 ||
|
|
159
|
+
(arg.type !== "NewExpression" &&
|
|
160
|
+
arg.type !== "ObjectExpression" &&
|
|
161
|
+
!(arg.type === "Literal" && typeof arg.value !== "string"))
|
|
162
|
+
) {
|
|
163
|
+
ok = false;
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!parent || parent.type !== "MemberExpression" || parent.object !== node || parent.computed) {
|
|
168
|
+
ok = false;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const member = parent.property.name;
|
|
172
|
+
if (name === reqName ? !REQ_OK.has(member) : !RES_OK.has(member)) {
|
|
173
|
+
ok = false;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return ok ? (usesNext ? SAFE_NEXT : SAFE) : NO;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Walks every node, handing each its parent. Arrays and nested objects are entered, nothing
|
|
181
|
+
* is interpreted: the judging happens in the visitor.
|
|
182
|
+
*
|
|
183
|
+
* @param {any} node
|
|
184
|
+
* @param {any} parent
|
|
185
|
+
* @param {(node: any, parent: any) => void} visit
|
|
186
|
+
*/
|
|
187
|
+
function walk(node, parent, visit) {
|
|
188
|
+
if (!node || typeof node.type !== "string") {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
visit(node, parent);
|
|
192
|
+
for (const key in node) {
|
|
193
|
+
if (key === "type" || key === "start" || key === "end") {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const value = node[key];
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
for (const item of value) {
|
|
199
|
+
if (item && typeof item.type === "string") {
|
|
200
|
+
walk(item, node, visit);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} else if (value && typeof value.type === "string") {
|
|
204
|
+
walk(value, node, visit);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether a native literal route's whole chain provably never reads a request header, so the
|
|
211
|
+
* request constructor may skip copying them and read the few framing headers directly.
|
|
212
|
+
*
|
|
213
|
+
* A callback that calls next passes anywhere but in the terminal route, where next would fall
|
|
214
|
+
* out of the chain: there it only passes when the caller established that no later route
|
|
215
|
+
* could catch the fall-through.
|
|
216
|
+
*
|
|
217
|
+
* @param {any[]} chain the routes the native handler runs, in order, this route last
|
|
218
|
+
* @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
|
|
219
|
+
* framework's own final handler
|
|
220
|
+
* @returns {boolean}
|
|
221
|
+
*/
|
|
222
|
+
function chainSkipsHeaders(chain, allowTerminalNext) {
|
|
223
|
+
for (let i = 0; i < chain.length; i++) {
|
|
224
|
+
const entry = chain[i];
|
|
225
|
+
const callbacks = entry.callbacks;
|
|
226
|
+
if (!Array.isArray(callbacks)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const terminal = i === chain.length - 1;
|
|
230
|
+
for (const cb of callbacks) {
|
|
231
|
+
if (typeof cb !== "function") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const verdict = callbackSkipsHeaders(cb);
|
|
235
|
+
if (verdict === NO || (verdict === SAFE_NEXT && terminal && !allowTerminalNext)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// a param callback runs code this walk never saw
|
|
240
|
+
if (entry.paramCallbacks && entry.paramCallbacks.size > 0) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = { chainSkipsHeaders, callbackSkipsHeaders, kGetSafe };
|