fulmine.js 5.19.2 → 5.19.4
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 +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/utils.js
CHANGED
|
@@ -28,6 +28,21 @@ const ms = require("ms");
|
|
|
28
28
|
const fs = require("fs");
|
|
29
29
|
const { Stats } = require("fs");
|
|
30
30
|
|
|
31
|
+
/** @typedef {import("./request.js")} Request */
|
|
32
|
+
/** @typedef {import("./response.js")} Response */
|
|
33
|
+
/**
|
|
34
|
+
* An error with the http-errors fields on it, which are what `res.status(err.status || 500)`, the
|
|
35
|
+
* error page and the body parsers read. None is required: a plain throw carries none of them.
|
|
36
|
+
* @typedef {Error & {
|
|
37
|
+
* status?: number,
|
|
38
|
+
* statusCode?: number,
|
|
39
|
+
* expose?: boolean,
|
|
40
|
+
* code?: string,
|
|
41
|
+
* type?: string,
|
|
42
|
+
* types?: string[]
|
|
43
|
+
* }} HttpError
|
|
44
|
+
*/
|
|
45
|
+
|
|
31
46
|
const EMPTY_REGEX = new RegExp(``);
|
|
32
47
|
|
|
33
48
|
// what express hands qs for a query string. allowPrototypes keeps a key named "constructor" or
|
|
@@ -231,11 +246,9 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
231
246
|
let lastWildcardEnd = -1;
|
|
232
247
|
// What path-to-regexp calls the wildcard backtrack: the literal text written since the last
|
|
233
248
|
// wildcard. Once a wildcard has eaten slashes, a later one in the same path is held to a single
|
|
234
|
-
// segment, or the two would divide the path
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// the text written since the last capture of any kind, and the text written since the last
|
|
238
|
-
// wildcard, which are the two path-to-regexp weighs
|
|
249
|
+
// segment, or the two would divide the path in more than one way and the regex would have to
|
|
250
|
+
// backtrack. /*a/*b against /x/y/ shows it: express refuses it under strict routing.
|
|
251
|
+
// Two counters: text since the last capture of any kind, and text since the last wildcard
|
|
239
252
|
let backtrack = "";
|
|
240
253
|
let wildcardBacktrack = "";
|
|
241
254
|
let lastCaptureWasWildcard = false;
|
|
@@ -405,16 +418,13 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
405
418
|
i++;
|
|
406
419
|
|
|
407
420
|
// When a :parameter precedes this group, that parameter is the one that gives ground
|
|
408
|
-
// while backtracking, so this one must not swallow the separator
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
// The whole
|
|
413
|
-
// foo=123 and bar=abc on express, and reading
|
|
414
|
-
// match its own text
|
|
415
|
-
// than one character cannot go in a class, so it is written as a lookahead, and either
|
|
416
|
-
// way the parameter is still allowed to be exactly the separator, as path-to-regexp
|
|
417
|
-
// writes it.
|
|
421
|
+
// while backtracking, so this one must not swallow the separator too. Express splits
|
|
422
|
+
// /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext cannot
|
|
423
|
+
// contain a dot. After static text nothing gives ground, so the parameter takes
|
|
424
|
+
// everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
|
|
425
|
+
// The whole separator, not its first character: /:foo{abc:bar} against /123abcabc
|
|
426
|
+
// splits as foo=123 and bar=abc on express, and reading it as "a" left bar unable to
|
|
427
|
+
// match its own text. More than one character cannot go in a class, so it is a lookahead
|
|
418
428
|
const colon = groupContent.indexOf(":");
|
|
419
429
|
const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
|
|
420
430
|
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -445,13 +455,11 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
445
455
|
}
|
|
446
456
|
}
|
|
447
457
|
if (lastWildcard && lastWildcardEnd === regexPattern.length) {
|
|
448
|
-
// A wildcard immediately before the group. `(?<w>[^]+)(?:group)?` can never let
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
// wildcard greedy in both, so that is what goes here. The second branch captures
|
|
454
|
-
// the same parameter under a name of its own, which is what uniqueGroupName is for.
|
|
458
|
+
// A wildcard immediately before the group. `(?<w>[^]+)(?:group)?` can never let the
|
|
459
|
+
// group match, because the wildcard is greedy and the group may be empty, and a
|
|
460
|
+
// lazy wildcard is not the same thing either: it gives the trailing slash away, and
|
|
461
|
+
// /*path{.:ext} against /a/b/ loses the empty last segment. path-to-regexp writes
|
|
462
|
+
// the two branches out instead, group first and the wildcard greedy in both
|
|
455
463
|
const second = uniqueGroupName(lastWildcard.name);
|
|
456
464
|
wildcardNames.push(second);
|
|
457
465
|
const withWildcard = regexPattern.slice(lastWildcard.start);
|
|
@@ -619,13 +627,13 @@ const NOT_A_LITERAL = /[:*{}\\]/;
|
|
|
619
627
|
*
|
|
620
628
|
* The answer is structural: no position where two different literals meet, and, when neither path
|
|
621
629
|
* can change length, the same number of segments. `/orders/:id` and `/invoices/:id` cannot both
|
|
622
|
-
* match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes
|
|
623
|
-
*
|
|
624
|
-
*
|
|
630
|
+
* match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes: saying two paths
|
|
631
|
+
* overlap only costs a native registration, while missing one lets uWS answer a request that
|
|
632
|
+
* belonged to an earlier route.
|
|
625
633
|
*
|
|
626
634
|
* A parameter is not the only shape that matches more than itself. A wildcard and an optional group
|
|
627
|
-
* do too, and reading `{:opt}` or `*splat` as
|
|
628
|
-
*
|
|
635
|
+
* do too, and reading `{:opt}` or `*splat` as literal text reported "cannot overlap" for a route
|
|
636
|
+
* that plainly could.
|
|
629
637
|
*
|
|
630
638
|
* @param {string} a
|
|
631
639
|
* @param {string} b
|
|
@@ -759,11 +767,8 @@ function acceptParams(str) {
|
|
|
759
767
|
//
|
|
760
768
|
// The keys are media types, so an application uses a handful and the ceiling is never approached.
|
|
761
769
|
// It is here because application code is free to hand res.type() something a client sent, and an
|
|
762
|
-
// unbounded map keyed on that is a leak the client controls.
|
|
763
|
-
//
|
|
764
|
-
// Clearing beats evicting one entry at a time: the few types an application really uses are back
|
|
765
|
-
// within a few requests, whereas refusing new entries once full would let a flood of invented
|
|
766
|
-
// values lock the real ones out for the life of the process.
|
|
770
|
+
// unbounded map keyed on that is a leak the client controls. Clearing beats evicting one at a
|
|
771
|
+
// time: refusing new entries once full would let a flood of invented values lock the real ones out.
|
|
767
772
|
const MEMO_LIMIT = 512;
|
|
768
773
|
|
|
769
774
|
/**
|
|
@@ -772,8 +777,9 @@ const MEMO_LIMIT = 512;
|
|
|
772
777
|
* The wrapped function must never answer undefined, since that is what the cache reads as a miss.
|
|
773
778
|
* The mime lookups here answer false for something they do not know, which caches correctly.
|
|
774
779
|
*
|
|
775
|
-
* @
|
|
776
|
-
* @
|
|
780
|
+
* @template T
|
|
781
|
+
* @param {(key: string) => T} fn
|
|
782
|
+
* @returns {(key: string) => T}
|
|
777
783
|
*/
|
|
778
784
|
function memoizeByString(fn) {
|
|
779
785
|
const cache = new Map();
|
|
@@ -818,7 +824,7 @@ function normalizeType(type) {
|
|
|
818
824
|
* unicode escapes, so a string in the body cannot close a script tag in an HTML page that embeds
|
|
819
825
|
* the response.
|
|
820
826
|
*
|
|
821
|
-
* @param {
|
|
827
|
+
* @param {unknown} value whatever the handler passed to res.json
|
|
822
828
|
* @param {any} [replacer] the "json replacer" setting
|
|
823
829
|
* @param {string|number} [spaces] the "json spaces" setting
|
|
824
830
|
* @param {boolean} [escape] the "json escape" setting
|
|
@@ -855,14 +861,12 @@ const ENCODING_ANY = ENCODING_BR | ENCODING_GZIP | ENCODING_DEFLATE | ENCODING_Z
|
|
|
855
861
|
/**
|
|
856
862
|
* The encoding to answer with, read straight off Accept-Encoding rather than through negotiator:
|
|
857
863
|
* the header is a short list of names with an optional q, and building a Negotiator per response
|
|
858
|
-
*
|
|
864
|
+
* costs more than the scan does. The tie-break is negotiator's, for the list the compression module
|
|
865
|
+
* hands it: brotli first, then gzip, then deflate, and identity last.
|
|
859
866
|
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
* Only the encodings named in `allowed` are on offer, since the caller may not be able to
|
|
864
|
-
* produce all three: express.static offers the two it can have lying on disk. An uncompressed
|
|
865
|
-
* answer is always on offer, and is what an empty header ends up choosing.
|
|
867
|
+
* Only the encodings named in `allowed` are on offer, since the caller may not produce all three:
|
|
868
|
+
* express.static offers the two it can have lying on disk. An uncompressed answer is always on
|
|
869
|
+
* offer, and is what an empty header chooses.
|
|
866
870
|
*
|
|
867
871
|
* @param {string} accept the header, or "" when the request carried none
|
|
868
872
|
* @param {number} allowed ENCODING_BR, ENCODING_ZSTD, ENCODING_GZIP and ENCODING_DEFLATE, or'd
|
|
@@ -989,12 +993,10 @@ const defaultSettings = {
|
|
|
989
993
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
990
994
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
991
995
|
"declarative responses": true,
|
|
992
|
-
// on. Off hands every request to the ordinary chain instead of letting
|
|
996
|
+
// on. Off hands every request to the ordinary chain instead of letting uWS match what it can,
|
|
993
997
|
// which is slower and answers the same. Not a tuning knob: it exists so one application can be
|
|
994
|
-
// served both ways and the
|
|
995
|
-
//
|
|
996
|
-
// `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
|
|
997
|
-
// takes "declarative responses" with it.
|
|
998
|
+
// served both ways and the answers compared, see `npm run fuzz -- --self`. A compiled response
|
|
999
|
+
// needs a native registration to hang on, so this takes "declarative responses" with it
|
|
998
1000
|
"native routes": true,
|
|
999
1001
|
// off: with a window set, the size and mtime of a file served by sendFile are remembered for
|
|
1000
1002
|
// it, which is one syscall less per request and a file that can be served as it was a moment
|
|
@@ -1053,13 +1055,17 @@ function cachedStat(file, ttl) {
|
|
|
1053
1055
|
/**
|
|
1054
1056
|
* A duration setting as milliseconds: false is off, a string is read by ms, a number is itself.
|
|
1055
1057
|
*
|
|
1056
|
-
* @param {
|
|
1058
|
+
* @param {string|number|boolean|undefined} value the setting as the application wrote it
|
|
1057
1059
|
* @param {string} name for the error, which names the setting the application wrote
|
|
1058
1060
|
* @returns {number}
|
|
1059
1061
|
*/
|
|
1060
1062
|
function durationSetting(value, name) {
|
|
1061
1063
|
const parsed =
|
|
1062
|
-
value === false || value === undefined
|
|
1064
|
+
value === false || value === undefined
|
|
1065
|
+
? 0
|
|
1066
|
+
: typeof value === "string"
|
|
1067
|
+
? ms(/** @type {import("ms").StringValue} */ (value))
|
|
1068
|
+
: value;
|
|
1063
1069
|
if (typeof parsed !== "number" || !(parsed >= 0)) {
|
|
1064
1070
|
throw new TypeError(`${name} must be a duration`);
|
|
1065
1071
|
}
|
|
@@ -1140,8 +1146,9 @@ function deprecated(oldMethod, newMethod, full = false) {
|
|
|
1140
1146
|
* request, each time picking up after the route it just ran, and Array.findIndex has no way to
|
|
1141
1147
|
* start anywhere but the beginning.
|
|
1142
1148
|
*
|
|
1143
|
-
* @
|
|
1144
|
-
* @param {
|
|
1149
|
+
* @template T
|
|
1150
|
+
* @param {T[]} arr
|
|
1151
|
+
* @param {(item: T, index: number, arr: T[]) => boolean} fn
|
|
1145
1152
|
* @param {number} [index] where to start
|
|
1146
1153
|
* @returns {number} the index, or -1
|
|
1147
1154
|
*/
|
|
@@ -1176,7 +1183,7 @@ function decode(path) {
|
|
|
1176
1183
|
*
|
|
1177
1184
|
* @param {string} value
|
|
1178
1185
|
* @returns {string}
|
|
1179
|
-
* @throws {
|
|
1186
|
+
* @throws {HttpError} carrying status 400 when the value cannot be decoded
|
|
1180
1187
|
*/
|
|
1181
1188
|
function decodeParam(value) {
|
|
1182
1189
|
// the common case, and worth the check: a parameter is usually a number or a word, and
|
|
@@ -1189,7 +1196,8 @@ function decodeParam(value) {
|
|
|
1189
1196
|
} catch {
|
|
1190
1197
|
// a URIError, not an Error: express throws what decodeURIComponent threw, so an error
|
|
1191
1198
|
// handler written as `err instanceof URIError` has to keep working here
|
|
1192
|
-
|
|
1199
|
+
/** @type {HttpError} */
|
|
1200
|
+
const err = new URIError(`Failed to decode param '${value}'`);
|
|
1193
1201
|
err.status = 400;
|
|
1194
1202
|
err.statusCode = 400;
|
|
1195
1203
|
err.expose = true;
|
|
@@ -1274,8 +1282,8 @@ function parseHttpDate(date) {
|
|
|
1274
1282
|
* no longer the current one, which is a 412 rather than a 304: the client asked to be stopped if
|
|
1275
1283
|
* anything had changed.
|
|
1276
1284
|
*
|
|
1277
|
-
* @param {
|
|
1278
|
-
* @param {
|
|
1285
|
+
* @param {Request} req
|
|
1286
|
+
* @param {Response} res
|
|
1279
1287
|
* @returns {boolean}
|
|
1280
1288
|
*/
|
|
1281
1289
|
function isPreconditionFailure(req, res) {
|
|
@@ -1296,7 +1304,8 @@ function isPreconditionFailure(req, res) {
|
|
|
1296
1304
|
// if-unmodified-since
|
|
1297
1305
|
const unmodifiedSince = parseHttpDate(req.headers["if-unmodified-since"]);
|
|
1298
1306
|
if (!isNaN(unmodifiedSince)) {
|
|
1299
|
-
|
|
1307
|
+
// cast because res.get answers an array for set-cookie, and never for this one
|
|
1308
|
+
const lastModified = parseHttpDate(/** @type {string|undefined} */ (res.get("Last-Modified")));
|
|
1300
1309
|
return isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
1301
1310
|
}
|
|
1302
1311
|
|
|
@@ -1345,7 +1354,7 @@ function statTag(stat, weak) {
|
|
|
1345
1354
|
* ETag comes from its size and mtime while a body's comes from its contents.
|
|
1346
1355
|
*
|
|
1347
1356
|
* @param {{weak: boolean}} options
|
|
1348
|
-
* @returns {(body:
|
|
1357
|
+
* @returns {(body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string}
|
|
1349
1358
|
*/
|
|
1350
1359
|
function createETagGenerator(options) {
|
|
1351
1360
|
return function generateETag(body, encoding) {
|
|
@@ -1367,24 +1376,26 @@ function createETagGenerator(options) {
|
|
|
1367
1376
|
* and sending the whole file. It may carry either an ETag or a date, and a date only counts when
|
|
1368
1377
|
* it matches Last-Modified exactly.
|
|
1369
1378
|
*
|
|
1370
|
-
* @param {
|
|
1371
|
-
* @param {
|
|
1379
|
+
* @param {Request} req
|
|
1380
|
+
* @param {Response} res
|
|
1372
1381
|
* @returns {boolean}
|
|
1373
1382
|
*/
|
|
1374
1383
|
function isRangeFresh(req, res) {
|
|
1375
|
-
|
|
1384
|
+
// folded to one string, as every header but set-cookie is
|
|
1385
|
+
const ifRange = /** @type {string|undefined} */ (req.headers["if-range"]);
|
|
1376
1386
|
if (!ifRange) {
|
|
1377
1387
|
return true;
|
|
1378
1388
|
}
|
|
1379
1389
|
|
|
1380
1390
|
// if-range as etag
|
|
1381
1391
|
if (ifRange.indexOf('"') !== -1) {
|
|
1382
|
-
const etag = res.get("etag");
|
|
1392
|
+
const etag = /** @type {string|undefined} */ (res.get("etag"));
|
|
1383
1393
|
return Boolean(etag && ifRange.indexOf(etag) !== -1);
|
|
1384
1394
|
}
|
|
1385
1395
|
|
|
1386
1396
|
// if-range as modified date
|
|
1387
|
-
|
|
1397
|
+
// cast because res.get answers an array for set-cookie, and never for this one
|
|
1398
|
+
const lastModified = /** @type {string|undefined} */ (res.get("Last-Modified"));
|
|
1388
1399
|
return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
|
|
1389
1400
|
}
|
|
1390
1401
|
|
|
@@ -1489,10 +1500,9 @@ const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
|
|
|
1489
1500
|
* One of node's header errors, built the way node builds it.
|
|
1490
1501
|
*
|
|
1491
1502
|
* Assigning the code is not the whole of it. Node also puts the code in the first line of the
|
|
1492
|
-
* stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking
|
|
1493
|
-
*
|
|
1494
|
-
*
|
|
1495
|
-
* "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing against express.
|
|
1503
|
+
* stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking the
|
|
1504
|
+
* name back off. The default error page prints exactly that: without this, the same refusal reads
|
|
1505
|
+
* "TypeError:" here and "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing.
|
|
1496
1506
|
*
|
|
1497
1507
|
* @param {string} message
|
|
1498
1508
|
* @param {string} code
|
|
@@ -1506,16 +1516,73 @@ function headerError(message, code) {
|
|
|
1506
1516
|
void err.stack;
|
|
1507
1517
|
// back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
|
|
1508
1518
|
// declares name as always present, and this deletes the own property to uncover it again
|
|
1509
|
-
delete (/** @type {
|
|
1519
|
+
delete (/** @type {{name?: string}} */ (err).name);
|
|
1510
1520
|
err.code = code;
|
|
1511
1521
|
return err;
|
|
1512
1522
|
}
|
|
1513
1523
|
|
|
1524
|
+
/**
|
|
1525
|
+
* node's ERR_HTTP_HEADERS_SENT, for a head that can no longer change: "set" from setHeader,
|
|
1526
|
+
* "remove" from removeHeader, "write" from writeHead, which is how node words each one.
|
|
1527
|
+
*
|
|
1528
|
+
* @param {string} verb
|
|
1529
|
+
* @returns {NodeJS.ErrnoException}
|
|
1530
|
+
*/
|
|
1531
|
+
function headersSentError(verb) {
|
|
1532
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1533
|
+
const err = new Error(`Cannot ${verb} headers after they are sent to the client`);
|
|
1534
|
+
// the first line of the stack reads as node's, see headerError
|
|
1535
|
+
err.name = "Error [ERR_HTTP_HEADERS_SENT]";
|
|
1536
|
+
void err.stack;
|
|
1537
|
+
delete (/** @type {{name?: string}} */ (err).name);
|
|
1538
|
+
err.code = "ERR_HTTP_HEADERS_SENT";
|
|
1539
|
+
return err;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Applies the headers a writeHead call carries, in either of node's shapes, (status, headers) or
|
|
1544
|
+
* (status, reason, headers), and answers the reason phrase when there was one. Shared with the
|
|
1545
|
+
* middleware that hooks writeHead: what it decides at the head has to see these first, the way
|
|
1546
|
+
* on-headers applies them before its listeners run.
|
|
1547
|
+
*
|
|
1548
|
+
* @param {{setHeader(name: string, value: import("http").OutgoingHttpHeader|undefined): unknown}} res
|
|
1549
|
+
* @param {string|import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [statusMessage]
|
|
1550
|
+
* @param {import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [headers]
|
|
1551
|
+
* @returns {string|undefined} the reason phrase
|
|
1552
|
+
*/
|
|
1553
|
+
function applyWriteHead(res, statusMessage, headers) {
|
|
1554
|
+
let reason;
|
|
1555
|
+
if (typeof statusMessage === "string") {
|
|
1556
|
+
reason = statusMessage;
|
|
1557
|
+
} else if (!headers) {
|
|
1558
|
+
// the two-argument shape, where what looked like a reason phrase is the headers
|
|
1559
|
+
headers = statusMessage;
|
|
1560
|
+
}
|
|
1561
|
+
if (Array.isArray(headers)) {
|
|
1562
|
+
// node takes a flat list here, name then value, and not a list of pairs. An odd length is
|
|
1563
|
+
// the caller's mistake and node names the argument in what it throws
|
|
1564
|
+
if (headers.length % 2 !== 0) {
|
|
1565
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1566
|
+
const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
|
|
1567
|
+
err.code = "ERR_INVALID_ARG_VALUE";
|
|
1568
|
+
throw err;
|
|
1569
|
+
}
|
|
1570
|
+
for (let i = 0; i < headers.length; i += 2) {
|
|
1571
|
+
res.setHeader(/** @type {string} */ (headers[i]), headers[i + 1]);
|
|
1572
|
+
}
|
|
1573
|
+
} else if (headers) {
|
|
1574
|
+
for (const header in headers) {
|
|
1575
|
+
res.setHeader(header, headers[header]);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return reason;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1514
1581
|
/**
|
|
1515
1582
|
* Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
|
|
1516
1583
|
* error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
|
|
1517
1584
|
*
|
|
1518
|
-
* @param {
|
|
1585
|
+
* @param {unknown} name whatever a caller passed as a header name, which is what is being checked
|
|
1519
1586
|
* @returns {void}
|
|
1520
1587
|
* @throws {TypeError} if the name is not a token, which includes not being a string
|
|
1521
1588
|
*/
|
|
@@ -1567,7 +1634,7 @@ function validateHeaderValue(name, value) {
|
|
|
1567
1634
|
* again is what turns one bad header into a dead process.
|
|
1568
1635
|
*
|
|
1569
1636
|
* @param {string} name
|
|
1570
|
-
* @param {any} value
|
|
1637
|
+
* @param {any} value whatever a caller passed as a header value
|
|
1571
1638
|
* @returns {boolean}
|
|
1572
1639
|
*/
|
|
1573
1640
|
function headerIsWritable(name, value) {
|
|
@@ -1589,21 +1656,39 @@ const STAT_ERROR_STATUS = { ENAMETOOLONG: 404, ENOTDIR: 404, ENOENT: 404 };
|
|
|
1589
1656
|
* of that handler as 500. The message is the status's own name, as http-errors writes it.
|
|
1590
1657
|
*
|
|
1591
1658
|
* @param {number} status
|
|
1592
|
-
* @
|
|
1659
|
+
* @param {string} [message] the status text unless given, as http-errors has it
|
|
1660
|
+
* @returns {HttpError}
|
|
1593
1661
|
*/
|
|
1594
|
-
function httpError(status) {
|
|
1595
|
-
|
|
1596
|
-
const err =
|
|
1662
|
+
function httpError(status, message = statuses.message[status] ?? "Error") {
|
|
1663
|
+
/** @type {HttpError} */
|
|
1664
|
+
const err = new Error(message);
|
|
1597
1665
|
// http-errors names these BadRequestError, ForbiddenError and so on, and the name is what the
|
|
1598
1666
|
// error page shows: an application looking at a 400 sees the same word Express shows it. Set
|
|
1599
1667
|
// before anything reads the stack, which V8 formats on first read
|
|
1600
|
-
err.name =
|
|
1668
|
+
err.name = httpErrorName(status);
|
|
1601
1669
|
err.expose = status < 500;
|
|
1602
1670
|
err.statusCode = status;
|
|
1603
1671
|
err.status = status;
|
|
1604
1672
|
return err;
|
|
1605
1673
|
}
|
|
1606
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* The name http-errors gives an error for a status, NotFoundError for 404: each word of the status
|
|
1677
|
+
* text capitalised and run together, plus Error unless it already ends in it, which is how
|
|
1678
|
+
* "Internal Server Error" stays InternalServerError.
|
|
1679
|
+
*
|
|
1680
|
+
* @param {number} status
|
|
1681
|
+
* @returns {string}
|
|
1682
|
+
*/
|
|
1683
|
+
function httpErrorName(status) {
|
|
1684
|
+
const name = (statuses.message[status] ?? "Error")
|
|
1685
|
+
.split(" ")
|
|
1686
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
1687
|
+
.join("")
|
|
1688
|
+
.replace(/[^ _0-9a-z]/gi, "");
|
|
1689
|
+
return name.endsWith("Error") ? name : name + "Error";
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1607
1692
|
/**
|
|
1608
1693
|
* Marks an fs error the way send does before it is handed on, so an error handler reading
|
|
1609
1694
|
* err.status or err.statusCode finds what it would find behind Express. The three properties are
|
|
@@ -1612,8 +1697,8 @@ function httpError(status) {
|
|
|
1612
1697
|
*
|
|
1613
1698
|
* The error itself is returned rather than a new one, so its errno, code, syscall and path survive.
|
|
1614
1699
|
*
|
|
1615
|
-
* @param {
|
|
1616
|
-
* @returns {
|
|
1700
|
+
* @param {HttpError} err the fs error, which carries its errno and path
|
|
1701
|
+
* @returns {HttpError} the same error
|
|
1617
1702
|
*/
|
|
1618
1703
|
function asStatError(err) {
|
|
1619
1704
|
err.expose = false;
|
|
@@ -1626,8 +1711,7 @@ function asStatError(err) {
|
|
|
1626
1711
|
// A constructor whose instances have no prototype, so a key from a request body or a query string
|
|
1627
1712
|
// cannot reach Object.prototype. Typed as returning a plain record: without that, assigning one
|
|
1628
1713
|
// reads as assigning `any`, which resets narrowing instead of removing undefined from it.
|
|
1629
|
-
/** @type {new () => Record<string, any>} */
|
|
1630
|
-
const NullObject = /** @type {any} */ (function () {});
|
|
1714
|
+
const NullObject = /** @type {new () => Record<string, any>} */ (/** @type {unknown} */ (function () {}));
|
|
1631
1715
|
NullObject.prototype = Object.create(null);
|
|
1632
1716
|
|
|
1633
1717
|
module.exports = {
|
|
@@ -1679,6 +1763,9 @@ module.exports = {
|
|
|
1679
1763
|
withUtf8Charset,
|
|
1680
1764
|
asStatError,
|
|
1681
1765
|
httpError,
|
|
1766
|
+
httpErrorName,
|
|
1767
|
+
headersSentError,
|
|
1768
|
+
applyWriteHead,
|
|
1682
1769
|
EMPTY_REGEX,
|
|
1683
1770
|
settingsEpoch
|
|
1684
1771
|
};
|
package/src/verify.js
CHANGED
|
@@ -16,16 +16,12 @@ limitations under the License.
|
|
|
16
16
|
|
|
17
17
|
// npx fulmine verify
|
|
18
18
|
//
|
|
19
|
-
// Whether this machine, and the image it will be deployed in, can run the thing at all. Not
|
|
20
|
-
//
|
|
21
|
-
// for: this is the question that comes before it, and it is the one that costs an hour when the
|
|
22
|
-
// answer is no and nobody asked.
|
|
19
|
+
// Whether this machine, and the image it will be deployed in, can run the thing at all. Not whether
|
|
20
|
+
// the application behaves the same, which is the test suite's job.
|
|
23
21
|
//
|
|
24
|
-
// There is a
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// Dockerfile written before any of this: each one fails at require time, in a container, in CI,
|
|
28
|
-
// with a message about a missing module that says nothing about what to do.
|
|
22
|
+
// There is a uWebSockets.js binary underneath, built per platform, per architecture and per node
|
|
23
|
+
// ABI, and linked against glibc. An Alpine image, a node version with no binary for it, a musl
|
|
24
|
+
// base: each one fails at require time with a message about a missing module.
|
|
29
25
|
//
|
|
30
26
|
// Thirty seconds here instead.
|
|
31
27
|
|
|
@@ -34,7 +30,7 @@ limitations under the License.
|
|
|
34
30
|
const fs = require("fs");
|
|
35
31
|
const path = require("path");
|
|
36
32
|
|
|
37
|
-
// The oldest glibc the pinned
|
|
33
|
+
// The oldest glibc the pinned uWS binaries are built against. A runtime older than this loads the
|
|
38
34
|
// file and then fails on a symbol, which is a worse error than not finding it at all.
|
|
39
35
|
const MIN_GLIBC = "2.38";
|
|
40
36
|
|
|
@@ -44,8 +40,8 @@ const MIN_NODE = require("../package.json").engines.node.replace(/[^0-9.]/g, "")
|
|
|
44
40
|
const MIN_NODE_MAJOR = MIN_NODE.split(".")[0];
|
|
45
41
|
const SWAP_IMAGE = `node:${MIN_NODE_MAJOR}-trixie-slim`;
|
|
46
42
|
|
|
47
|
-
// What a project may carry that needs a different API here
|
|
48
|
-
//
|
|
43
|
+
// What a project may carry that needs a different API here. Everything that just works is
|
|
44
|
+
// `npx fulmine migrate`'s business.
|
|
49
45
|
const NEEDS_A_LOOK = {
|
|
50
46
|
"socket.io": "attach it with io.attachApp(app.uwsApp), not io.attach(server): there is no node socket to take over",
|
|
51
47
|
ws: "the websocket server is µWS's own, through app.ws(path, behavior)",
|
|
@@ -55,9 +51,8 @@ const NEEDS_A_LOOK = {
|
|
|
55
51
|
};
|
|
56
52
|
|
|
57
53
|
/**
|
|
58
|
-
* One line of the report.
|
|
59
|
-
*
|
|
60
|
-
* something to read, not something to fail a pipeline over.
|
|
54
|
+
* One line of the report. Only "no" is a failure: an image that cannot load the binary stops the
|
|
55
|
+
* deployment, a dependency that wants a different call does not.
|
|
61
56
|
*
|
|
62
57
|
* @param {"ok"|"note"|"no"} level
|
|
63
58
|
* @param {string} what
|
|
@@ -92,9 +87,8 @@ function atLeast(version, minimum) {
|
|
|
92
87
|
/**
|
|
93
88
|
* The node this is running on, against what the package asks for.
|
|
94
89
|
*
|
|
95
|
-
* The version
|
|
96
|
-
*
|
|
97
|
-
* takes what it judges for the same reason.
|
|
90
|
+
* The version is an argument rather than read here, so a node this machine is not running is still
|
|
91
|
+
* testable. Every check below takes what it judges for the same reason.
|
|
98
92
|
*
|
|
99
93
|
* @param {string} [running] defaults to the node running this
|
|
100
94
|
* @param {string} [required] defaults to what package.json asks for
|
|
@@ -115,15 +109,15 @@ function checkNode(running = process.versions.node, required = require("../packa
|
|
|
115
109
|
* @returns {string|undefined}
|
|
116
110
|
*/
|
|
117
111
|
function currentGlibc() {
|
|
118
|
-
return /** @type {
|
|
112
|
+
return /** @type {{header: {glibcVersionRuntime?: string}}} */ (process.report.getReport()).header
|
|
113
|
+
.glibcVersionRuntime;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
/**
|
|
122
117
|
* Whether the C library is the one the binaries are linked against. Only linux has two of them.
|
|
123
118
|
*
|
|
124
|
-
* Both arguments are required
|
|
125
|
-
*
|
|
126
|
-
* case into whatever this machine happens to run. Reading the machine is the caller's job.
|
|
119
|
+
* Both arguments are required on purpose: undefined means musl, and a default parameter fires on
|
|
120
|
+
* an explicit undefined, so a default would turn the musl case into whatever this machine runs.
|
|
127
121
|
*
|
|
128
122
|
* @param {string} platform
|
|
129
123
|
* @param {string|undefined} glibc the runtime glibc, absent on musl
|
|
@@ -152,9 +146,8 @@ function checkLibc(platform, glibc) {
|
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
/**
|
|
155
|
-
* Whether there is a
|
|
156
|
-
*
|
|
157
|
-
* loaded first, so the answer says which of the three does not line up.
|
|
149
|
+
* Whether there is a uWebSockets.js binary for this platform, architecture and node ABI. The file
|
|
150
|
+
* is named rather than loaded, so the answer says which of the three does not line up.
|
|
158
151
|
*
|
|
159
152
|
* @param {string} [platform]
|
|
160
153
|
* @param {string} [arch]
|
|
@@ -214,12 +207,11 @@ function checkBinary(platform = process.platform, arch = process.arch, abi = pro
|
|
|
214
207
|
*/
|
|
215
208
|
function abiToNode(abi) {
|
|
216
209
|
const known = { 108: "18", 115: "20", 127: "22", 131: "23", 137: "24", 147: "26" };
|
|
217
|
-
return
|
|
210
|
+
return known[abi] ?? `ABI ${abi}`;
|
|
218
211
|
}
|
|
219
212
|
|
|
220
213
|
/**
|
|
221
|
-
* The base images a Dockerfile names, which is where the musl question is usually answered
|
|
222
|
-
* anybody meaning to.
|
|
214
|
+
* The base images a Dockerfile names, which is where the musl question is usually answered.
|
|
223
215
|
*
|
|
224
216
|
* @param {string} dir the project being verified
|
|
225
217
|
* @returns {ReturnType<typeof result>[]}
|
|
@@ -278,7 +270,7 @@ function checkDependencies(dir) {
|
|
|
278
270
|
const installed = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
279
271
|
for (const name of Object.keys(NEEDS_A_LOOK)) {
|
|
280
272
|
if (installed[name]) {
|
|
281
|
-
results.push(result("note", `${name} needs a different API here`,
|
|
273
|
+
results.push(result("note", `${name} needs a different API here`, NEEDS_A_LOOK[name]));
|
|
282
274
|
}
|
|
283
275
|
}
|
|
284
276
|
return results;
|
|
@@ -309,8 +301,7 @@ function verify(argv) {
|
|
|
309
301
|
console.log(` ${detail}`);
|
|
310
302
|
}
|
|
311
303
|
}
|
|
312
|
-
// only a blocked start is a failure
|
|
313
|
-
// reading and is not worth failing a pipeline over
|
|
304
|
+
// only a blocked start is a failure, a dependency that wants a different call is not
|
|
314
305
|
const blocking = results.filter((entry) => entry.level === "no").length;
|
|
315
306
|
const notes = results.filter((entry) => entry.level === "note").length;
|
|
316
307
|
console.log(
|
package/src/view.js
CHANGED
|
@@ -73,9 +73,8 @@ module.exports = class View {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* The first
|
|
77
|
-
*
|
|
78
|
-
* so an application can put its own templates in front of a package's.
|
|
76
|
+
* The first configured root that holds this template, or undefined. `views` may be one
|
|
77
|
+
* directory or a list, and the list is searched in order.
|
|
79
78
|
*
|
|
80
79
|
* @param {string} name template file name, relative to a root
|
|
81
80
|
* @returns {string|undefined} absolute path to the file that exists
|
|
@@ -100,10 +99,9 @@ module.exports = class View {
|
|
|
100
99
|
/**
|
|
101
100
|
* Renders the template through its engine.
|
|
102
101
|
*
|
|
103
|
-
* The callback is always delivered asynchronously, even when the engine answers on the spot
|
|
104
|
-
* `sync` is
|
|
105
|
-
*
|
|
106
|
-
* orders. Express normalises it the same way.
|
|
102
|
+
* The callback is always delivered asynchronously, even when the engine answers on the spot:
|
|
103
|
+
* `sync` is true only if the engine called back before this returned, and then the callback
|
|
104
|
+
* goes to the next tick. Express does the same.
|
|
107
105
|
*
|
|
108
106
|
* @param {Record<string, any>} options locals and engine options, passed through untouched
|
|
109
107
|
* @param {Function} callback called with whatever the engine passed, which is normally
|
|
@@ -115,7 +113,7 @@ module.exports = class View {
|
|
|
115
113
|
this.engine(
|
|
116
114
|
this.path,
|
|
117
115
|
options,
|
|
118
|
-
/** @this {
|
|
116
|
+
/** @this {unknown} */ function onRender() {
|
|
119
117
|
if (!sync) {
|
|
120
118
|
return callback.apply(this, arguments);
|
|
121
119
|
}
|