fulmine.js 5.21.4 → 5.21.6
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 +1 -1
- package/src/index.js +0 -21
- package/src/middlewares.js +47 -6
- package/src/node-shim.js +50 -12
- package/src/request.js +3 -2
- package/src/response-utils.js +24 -1
- package/src/response.js +119 -14
- package/src/uws.js +25 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -17,27 +17,6 @@ See the License for the specific language governing permissions and
|
|
|
17
17
|
limitations under the License.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
// A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
|
|
21
|
-
// one installed can drift from the one this package pins and was tested against. Said once, at
|
|
22
|
-
// require time, where it reaches every deployment rather than only the ones that run verify.
|
|
23
|
-
{
|
|
24
|
-
const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
|
|
25
|
-
/** @type {string|undefined} */
|
|
26
|
-
let installed;
|
|
27
|
-
try {
|
|
28
|
-
// its exports map does not expose package.json, so it is read beside the entry point
|
|
29
|
-
const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
|
|
30
|
-
installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
|
|
31
|
-
} catch {
|
|
32
|
-
// nothing to compare against, which is not worth a warning of its own
|
|
33
|
-
}
|
|
34
|
-
if (pinned && installed && installed !== pinned) {
|
|
35
|
-
console.warn(
|
|
36
|
-
`fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
|
|
37
|
-
" On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
20
|
const Application = require("./application.js");
|
|
42
21
|
const Router = require("./router.js");
|
|
43
22
|
const Route = require("./route.js");
|
package/src/middlewares.js
CHANGED
|
@@ -31,13 +31,43 @@ const parseQuery = require("./parse-query.js");
|
|
|
31
31
|
const { kGetSafe } = require("./usage.js");
|
|
32
32
|
const { AsyncResource } = require("async_hooks");
|
|
33
33
|
|
|
34
|
+
// node's unexported Symbol("context_frame"), where an AsyncResource keeps the store it captured,
|
|
35
|
+
// read off the first resource. null when node has none, then every binding keeps its scope
|
|
36
|
+
/** @type {symbol|null|undefined} */
|
|
37
|
+
let kContextFrame;
|
|
38
|
+
// set once an init hook was seen, never cleared: from then on every binding has its scope as before
|
|
39
|
+
let initHooksSeen = false;
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
|
-
* AsyncResource.bind without node's generic wrapper
|
|
42
|
+
* raw-body's wrap: AsyncResource.bind without node's generic wrapper (~1.9us per call there), and no
|
|
43
|
+
* scope without an AsyncLocalStorage store or an init hook: 2.3 to 3.5us per request in a uWS
|
|
44
|
+
* callback, +9 to 16% on the arena's POST. new AsyncResource("") throws only while a hook listens.
|
|
36
45
|
*
|
|
37
46
|
* @param {(...args: any[]) => any} fn called with at most one argument by every caller here
|
|
38
47
|
* @returns {(err?: any) => any}
|
|
39
48
|
*/
|
|
40
49
|
function bindContext(fn) {
|
|
50
|
+
if (!initHooksSeen) {
|
|
51
|
+
/** @type {AsyncResource|undefined} */
|
|
52
|
+
let probe;
|
|
53
|
+
try {
|
|
54
|
+
probe = new AsyncResource("");
|
|
55
|
+
} catch {
|
|
56
|
+
initHooksSeen = true;
|
|
57
|
+
}
|
|
58
|
+
if (probe !== undefined) {
|
|
59
|
+
if (kContextFrame === undefined) {
|
|
60
|
+
kContextFrame =
|
|
61
|
+
Object.getOwnPropertySymbols(probe).find((s) => s.description === "context_frame") ?? null;
|
|
62
|
+
}
|
|
63
|
+
if (kContextFrame !== null && /** @type {any} */ (probe)[kContextFrame] === undefined) {
|
|
64
|
+
return fn;
|
|
65
|
+
}
|
|
66
|
+
// a store to carry, and no init hook to have seen the empty type: the probe is the resource
|
|
67
|
+
const resource = probe;
|
|
68
|
+
return (err) => resource.runInAsyncScope(fn, undefined, err);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
41
71
|
const resource = new AsyncResource(fn.name || "bound-anonymous-fn");
|
|
42
72
|
return (err) => resource.runInAsyncScope(fn, undefined, err);
|
|
43
73
|
}
|
|
@@ -586,8 +616,14 @@ function serveStatic(root, options) {
|
|
|
586
616
|
}
|
|
587
617
|
const ext = path.extname(fullpath);
|
|
588
618
|
let i = 0;
|
|
589
|
-
// no extension on a directory; on the decoded url, as send
|
|
590
|
-
|
|
619
|
+
// no extension on a directory; on the decoded url and only after ENOENT, as send does:
|
|
620
|
+
// ENOTDIR or ENAMETOOLONG report the path asked, not the last extension (fuzz seed 3561875562)
|
|
621
|
+
if (
|
|
622
|
+
/** @type {NodeJS.ErrnoException} */ (err).code === "ENOENT" &&
|
|
623
|
+
ext === "" &&
|
|
624
|
+
!url.endsWith("/") &&
|
|
625
|
+
options.extensions
|
|
626
|
+
) {
|
|
591
627
|
while (i < options.extensions.length) {
|
|
592
628
|
try {
|
|
593
629
|
stat = fs.statSync(fullpath + "." + options.extensions[i]);
|
|
@@ -1174,9 +1210,14 @@ const json = createBodyParser(
|
|
|
1174
1210
|
|
|
1175
1211
|
// strict: only an object or an array is a body
|
|
1176
1212
|
if (options.strict !== false) {
|
|
1177
|
-
//
|
|
1178
|
-
//
|
|
1179
|
-
|
|
1213
|
+
// the four characters body-parser skips, space, tab, LF and CR, with a loop: the regex
|
|
1214
|
+
// allocated a match array per body, 56ns to 14
|
|
1215
|
+
let at = 0;
|
|
1216
|
+
let code = text.charCodeAt(0);
|
|
1217
|
+
while (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) {
|
|
1218
|
+
code = text.charCodeAt(++at);
|
|
1219
|
+
}
|
|
1220
|
+
const first = at < text.length ? text[at] : undefined;
|
|
1180
1221
|
if (first !== "{" && first !== "[") {
|
|
1181
1222
|
// a SyntaxError, as body-parser builds it
|
|
1182
1223
|
return next(
|
package/src/node-shim.js
CHANGED
|
@@ -15,14 +15,36 @@ limitations under the License.
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
// A uWS-shaped request and response over node's own, for supertest and http.createServer(app).
|
|
18
|
-
// Not fast
|
|
19
|
-
// callback, a status is kept until the first
|
|
18
|
+
// Not the fast path, app.listen() is, but about 1.5x Express on a hello world on Linux. Where the
|
|
19
|
+
// models disagree node's gives way: cork only runs its callback, a status is kept until the first
|
|
20
|
+
// byte of body
|
|
20
21
|
|
|
21
22
|
const { IncomingMessage } = require("http");
|
|
23
|
+
const { HEADER_NAME_BUF, HEADER_VALUE_BUF } = require("./response-utils.js");
|
|
22
24
|
|
|
23
25
|
/** What µWS returns for an address nobody declared. */
|
|
24
26
|
const emptyAddress = new ArrayBuffer(0);
|
|
25
27
|
|
|
28
|
+
// The strings behind the Buffers writeHeaders hands over for the recurring names and values.
|
|
29
|
+
// Decoded on every response they gave node a new string to check, lowercase and hash each time
|
|
30
|
+
/** @type {Map<unknown, string>} */
|
|
31
|
+
const HEADER_TEXT = new Map();
|
|
32
|
+
for (const cache of [HEADER_NAME_BUF, HEADER_VALUE_BUF]) {
|
|
33
|
+
for (const text in cache) {
|
|
34
|
+
HEADER_TEXT.set(cache[text], text);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A header name or value as node's appendHeader takes it.
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} part
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function headerText(part) {
|
|
45
|
+
return typeof part === "string" ? part : (HEADER_TEXT.get(part) ?? String(part));
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
/**
|
|
27
49
|
* An IP address as the four or sixteen bytes uWS hands over; unreadable comes back empty.
|
|
28
50
|
*
|
|
@@ -93,6 +115,22 @@ function toArrayBuffer(chunk) {
|
|
|
93
115
|
return /** @type {ArrayBuffer} */ (buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
|
|
94
116
|
}
|
|
95
117
|
|
|
118
|
+
/**
|
|
119
|
+
* A body chunk as node's write takes it: a string goes as it is and node encodes it once, with
|
|
120
|
+
* the head, anything else is copied as toArrayBuffer does.
|
|
121
|
+
*
|
|
122
|
+
* @param {ArrayBuffer|Buffer|string} chunk
|
|
123
|
+
* @returns {Buffer|string}
|
|
124
|
+
*/
|
|
125
|
+
function toNodeChunk(chunk) {
|
|
126
|
+
return typeof chunk === "string" ? chunk : Buffer.from(toArrayBuffer(chunk));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @param {Buffer|string} chunk */
|
|
130
|
+
function byteLength(chunk) {
|
|
131
|
+
return typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
|
|
132
|
+
}
|
|
133
|
+
|
|
96
134
|
/** uWS's HttpRequest over node's IncomingMessage. */
|
|
97
135
|
class NodeHttpRequest {
|
|
98
136
|
/** @type {import("http").IncomingMessage} */
|
|
@@ -241,16 +279,16 @@ class NodeHttpResponse {
|
|
|
241
279
|
writeHeader(key, value) {
|
|
242
280
|
if (!this._nodeRes.headersSent) {
|
|
243
281
|
// writeHeaders hands the recurring names and values over as Buffers
|
|
244
|
-
this._nodeRes.appendHeader(
|
|
282
|
+
this._nodeRes.appendHeader(headerText(key), headerText(value));
|
|
245
283
|
}
|
|
246
284
|
return this;
|
|
247
285
|
}
|
|
248
286
|
|
|
249
287
|
/** @param {ArrayBuffer|Buffer|string} chunk @returns {boolean} false when the socket is full */
|
|
250
288
|
write(chunk) {
|
|
251
|
-
const
|
|
252
|
-
this._offset +=
|
|
253
|
-
return this._nodeRes.write(
|
|
289
|
+
const data = toNodeChunk(chunk);
|
|
290
|
+
this._offset += byteLength(data);
|
|
291
|
+
return this._nodeRes.write(data);
|
|
254
292
|
}
|
|
255
293
|
|
|
256
294
|
/** @param {ArrayBuffer|Buffer|string} [body] */
|
|
@@ -262,9 +300,9 @@ class NodeHttpResponse {
|
|
|
262
300
|
this._nodeRes.end();
|
|
263
301
|
return this;
|
|
264
302
|
}
|
|
265
|
-
const
|
|
266
|
-
this._offset +=
|
|
267
|
-
this._nodeRes.end(
|
|
303
|
+
const data = toNodeChunk(body);
|
|
304
|
+
this._offset += byteLength(data);
|
|
305
|
+
this._nodeRes.end(data);
|
|
268
306
|
return this;
|
|
269
307
|
}
|
|
270
308
|
|
|
@@ -298,9 +336,9 @@ class NodeHttpResponse {
|
|
|
298
336
|
if (!this._nodeRes.headersSent && !this._nodeRes.hasHeader("Content-Length")) {
|
|
299
337
|
this._nodeRes.setHeader("Content-Length", String(totalSize));
|
|
300
338
|
}
|
|
301
|
-
const
|
|
302
|
-
const ok = this._nodeRes.write(
|
|
303
|
-
this._offset +=
|
|
339
|
+
const data = toNodeChunk(chunk);
|
|
340
|
+
const ok = this._nodeRes.write(data);
|
|
341
|
+
this._offset += byteLength(data);
|
|
304
342
|
const done = this._offset >= totalSize;
|
|
305
343
|
if (done) {
|
|
306
344
|
this._nodeRes.end();
|
package/src/request.js
CHANGED
|
@@ -999,8 +999,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
999
999
|
/** @type {string|undefined} */
|
|
1000
1000
|
let ip;
|
|
1001
1001
|
if (rawIp.byteLength === 4) {
|
|
1002
|
-
// plain, as node writes an IPv4 peer
|
|
1003
|
-
|
|
1002
|
+
// plain, as node writes an IPv4 peer; by hand, Uint8Array join() was 420us per 1000 requests
|
|
1003
|
+
const bytes = new Uint8Array(rawIp);
|
|
1004
|
+
ip = bytes[0] + "." + bytes[1] + "." + bytes[2] + "." + bytes[3];
|
|
1004
1005
|
} else if (rawIp.byteLength === 16) {
|
|
1005
1006
|
const bytes = new Uint8Array(rawIp);
|
|
1006
1007
|
if (isMappedIPv4(bytes)) {
|
package/src/response-utils.js
CHANGED
|
@@ -34,8 +34,30 @@ const kShapeMode =
|
|
|
34
34
|
Symbol("shapeMode");
|
|
35
35
|
// names setHeader has validated and lowercased, so the constant names middleware writes per
|
|
36
36
|
// request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
|
|
37
|
-
// the never-throwing readers
|
|
37
|
+
// the never-throwing readers have HEADER_KEYS below
|
|
38
38
|
const VALIDATED_HEADER_NAMES = new Map();
|
|
39
|
+
// lowercase names for the header readers, apart from the map above as nothing here is validated.
|
|
40
|
+
// A fresh lowercase key goes through the V8 runtime on res.headers: 76ns against 6 cached, and
|
|
41
|
+
// helmet's removeHeader("X-Powered-By") alone was 190us per thousand requests. Bounded too
|
|
42
|
+
/** @type {Map<string, string>} */
|
|
43
|
+
const HEADER_KEYS = new Map();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The key a header lives under in res.headers, for the readers.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} field the name as the caller wrote it
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function headerKey(field) {
|
|
52
|
+
let key = HEADER_KEYS.get(field);
|
|
53
|
+
if (key === undefined) {
|
|
54
|
+
key = field.toLowerCase();
|
|
55
|
+
if (HEADER_KEYS.size < 512 && typeof field === "string") {
|
|
56
|
+
HEADER_KEYS.set(field, key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return key;
|
|
60
|
+
}
|
|
39
61
|
// The names and values that recur on every response, kept as Buffers for the uWS crossing: a Buffer
|
|
40
62
|
// is memcpy'd as it is, a string pays a UTF-8 scan and copy per call. A header that is not here
|
|
41
63
|
// crosses as the string it was. Names must stay lowercase, as writeHeaders receives them. The
|
|
@@ -121,6 +143,7 @@ module.exports = {
|
|
|
121
143
|
kOutHeaders,
|
|
122
144
|
kShapeMode,
|
|
123
145
|
VALIDATED_HEADER_NAMES,
|
|
146
|
+
headerKey,
|
|
124
147
|
HEADER_NAME_BUF,
|
|
125
148
|
HEADER_VALUE_BUF,
|
|
126
149
|
statusLine,
|
package/src/response.js
CHANGED
|
@@ -57,6 +57,7 @@ const {
|
|
|
57
57
|
kOutHeaders,
|
|
58
58
|
kShapeMode,
|
|
59
59
|
VALIDATED_HEADER_NAMES,
|
|
60
|
+
headerKey,
|
|
60
61
|
HEADER_NAME_BUF,
|
|
61
62
|
HEADER_VALUE_BUF,
|
|
62
63
|
statusLine,
|
|
@@ -77,6 +78,10 @@ const JSON_UTF8 = "application/json; charset=utf-8";
|
|
|
77
78
|
|
|
78
79
|
// the seeded Keep-Alive, a constant so setHeader can tell it from one the application set
|
|
79
80
|
const SEEDED_KEEP_ALIVE = "timeout=10";
|
|
81
|
+
// the seeded connection and keep-alive as one value, every response writes them in one call:
|
|
82
|
+
// 572ns to 438 per head with a content-type, on node 26
|
|
83
|
+
const SEEDED_PAIR = "keep-alive\r\nkeep-alive: " + SEEDED_KEEP_ALIVE;
|
|
84
|
+
const SEEDED_PAIR_BUF = Buffer.from(SEEDED_PAIR);
|
|
80
85
|
// send's ceiling for maxAge, one year
|
|
81
86
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
82
87
|
|
|
@@ -600,8 +605,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
600
605
|
|
|
601
606
|
/**
|
|
602
607
|
* Writes every header set so far to uWS, the point of no return. Content-Length is kept on
|
|
603
|
-
* totalSize instead: uWS takes the length through tryEnd or endWithoutBody.
|
|
604
|
-
*
|
|
608
|
+
* totalSize instead: uWS takes the length through tryEnd or endWithoutBody.
|
|
609
|
+
*
|
|
610
|
+
* From the third line on the head goes in one writeHeader, joined in the first value, as uWS
|
|
611
|
+
* writes `name: value\r\n` without reading it. Per head on node 26: helmet's set 3953ns to 1327,
|
|
612
|
+
* five lines 1013 to 531. One or two lines keep a call each, the join costs what it saves (issue #11).
|
|
605
613
|
*
|
|
606
614
|
* @param {boolean} utf8 unused, kept because node's equivalent takes it and the two callers
|
|
607
615
|
* differ on what they know about the body
|
|
@@ -614,6 +622,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
614
622
|
// length first: the value is nearly always "keep-alive", which paid a lowercase per response
|
|
615
623
|
const closing =
|
|
616
624
|
typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
|
|
625
|
+
// the node shim writes a line with node's appendHeader, that refuses the CRLF of a join
|
|
626
|
+
const joins = res._nodeRes === undefined;
|
|
627
|
+
// lines not written yet: the first two apart, from the third on in joined
|
|
628
|
+
let lines = 0;
|
|
629
|
+
let pair = false;
|
|
630
|
+
let firstName = "";
|
|
631
|
+
let firstValue = "";
|
|
632
|
+
let secondName = "";
|
|
633
|
+
let secondValue = "";
|
|
634
|
+
let joined = "";
|
|
617
635
|
// for..in over an object a few responses delete from (204, 304, 205, a 304 file,
|
|
618
636
|
// removeHeader), rare enough to keep the shape, unlike the request side
|
|
619
637
|
for (const header in headers) {
|
|
@@ -637,20 +655,107 @@ module.exports = class Response extends LazyWritable {
|
|
|
637
655
|
this.#userChunked = true;
|
|
638
656
|
continue;
|
|
639
657
|
}
|
|
640
|
-
//
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
658
|
+
// a string first, nearly every value is one
|
|
659
|
+
const isText = typeof value === "string";
|
|
660
|
+
const list = isText || !Array.isArray(value) ? null : value;
|
|
661
|
+
// only text is joined, the rest crosses alone as before. false is joined as "false", the
|
|
662
|
+
// content-type res.set stores for an unknown type
|
|
663
|
+
let joinable = joins && (isText || list !== null || value === false);
|
|
664
|
+
if (joinable && list !== null) {
|
|
665
|
+
for (let i = 0; i < list.length; i++) {
|
|
666
|
+
if (typeof list[i] !== "string") {
|
|
667
|
+
joinable = false;
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
645
670
|
}
|
|
646
|
-
} else {
|
|
647
|
-
res.writeHeader(name, HEADER_VALUE_BUF[value] || value);
|
|
648
671
|
}
|
|
672
|
+
if (!joinable) {
|
|
673
|
+
// the lines before this one go first, so the order on the wire holds
|
|
674
|
+
if (lines !== 0) {
|
|
675
|
+
Response.#writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined);
|
|
676
|
+
lines = 0;
|
|
677
|
+
pair = false;
|
|
678
|
+
}
|
|
679
|
+
// the recurring names and values cross as cached Buffers, see HEADER_NAME_BUF
|
|
680
|
+
const name = HEADER_NAME_BUF[header] || header;
|
|
681
|
+
if (list !== null) {
|
|
682
|
+
for (const val of list) {
|
|
683
|
+
res.writeHeader(name, HEADER_VALUE_BUF[val] || val);
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
res.writeHeader(name, HEADER_VALUE_BUF[value] || value);
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
const count = list === null ? 1 : list.length;
|
|
691
|
+
for (let i = 0; i < count; i++) {
|
|
692
|
+
const text = list === null ? (value === false ? "false" : value) : list[i];
|
|
693
|
+
if (
|
|
694
|
+
lines === 1 &&
|
|
695
|
+
!pair &&
|
|
696
|
+
header === "keep-alive" &&
|
|
697
|
+
text === SEEDED_KEEP_ALIVE &&
|
|
698
|
+
firstName === "connection" &&
|
|
699
|
+
firstValue === "keep-alive"
|
|
700
|
+
) {
|
|
701
|
+
// the seeded pair, one line from here on
|
|
702
|
+
pair = true;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (lines === 0) {
|
|
706
|
+
firstName = header;
|
|
707
|
+
firstValue = text;
|
|
708
|
+
} else if (lines === 1) {
|
|
709
|
+
secondName = header;
|
|
710
|
+
secondValue = text;
|
|
711
|
+
} else if (lines === 2) {
|
|
712
|
+
joined =
|
|
713
|
+
(pair ? SEEDED_PAIR : firstValue) +
|
|
714
|
+
"\r\n" +
|
|
715
|
+
secondName +
|
|
716
|
+
": " +
|
|
717
|
+
secondValue +
|
|
718
|
+
"\r\n" +
|
|
719
|
+
header +
|
|
720
|
+
": " +
|
|
721
|
+
text;
|
|
722
|
+
} else {
|
|
723
|
+
joined += "\r\n" + header + ": " + text;
|
|
724
|
+
}
|
|
725
|
+
lines++;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (lines !== 0) {
|
|
729
|
+
Response.#writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined);
|
|
649
730
|
}
|
|
650
731
|
this.headersSent = true;
|
|
651
732
|
this.#headOut = true;
|
|
652
733
|
}
|
|
653
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Writes the lines of writeHeaders: one or two a call each, three or more in one call.
|
|
737
|
+
*
|
|
738
|
+
* @param {import("uWebSockets.js").HttpResponse} res
|
|
739
|
+
* @param {number} lines how many, the seeded pair counted as one
|
|
740
|
+
* @param {boolean} pair whether the first line is the seeded pair
|
|
741
|
+
* @param {string} firstName
|
|
742
|
+
* @param {string} firstValue
|
|
743
|
+
* @param {string} secondName
|
|
744
|
+
* @param {string} secondValue
|
|
745
|
+
* @param {string} joined the first value with the other lines after it
|
|
746
|
+
*/
|
|
747
|
+
static #writeLines(res, lines, pair, firstName, firstValue, secondName, secondValue, joined) {
|
|
748
|
+
const name = HEADER_NAME_BUF[firstName] || firstName;
|
|
749
|
+
if (lines > 2) {
|
|
750
|
+
res.writeHeader(name, joined);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
res.writeHeader(name, pair ? SEEDED_PAIR_BUF : HEADER_VALUE_BUF[firstValue] || firstValue);
|
|
754
|
+
if (lines === 2) {
|
|
755
|
+
res.writeHeader(HEADER_NAME_BUF[secondName] || secondName, HEADER_VALUE_BUF[secondValue] || secondValue);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
654
759
|
/**
|
|
655
760
|
* node's, called before a body when writeHead never was. Guarded here: the compression module
|
|
656
761
|
* calls it on the strength of node's _header, which this response does not keep.
|
|
@@ -1473,7 +1578,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1473
1578
|
* @returns {boolean}
|
|
1474
1579
|
*/
|
|
1475
1580
|
hasHeader(name) {
|
|
1476
|
-
return this.headers[name
|
|
1581
|
+
return this.headers[headerKey(name)] !== undefined;
|
|
1477
1582
|
}
|
|
1478
1583
|
|
|
1479
1584
|
/**
|
|
@@ -1502,7 +1607,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1502
1607
|
* @returns {this}
|
|
1503
1608
|
*/
|
|
1504
1609
|
appendHeader(name, value) {
|
|
1505
|
-
const key = name
|
|
1610
|
+
const key = headerKey(name);
|
|
1506
1611
|
const current = this.headers[key];
|
|
1507
1612
|
if (current === undefined) {
|
|
1508
1613
|
return this.setHeader(name, value);
|
|
@@ -1563,7 +1668,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1563
1668
|
this.set(header, fields[header]);
|
|
1564
1669
|
}
|
|
1565
1670
|
} else {
|
|
1566
|
-
const name = field
|
|
1671
|
+
const name = headerKey(field);
|
|
1567
1672
|
// coerced here as Express does, so res.get answers what was sent
|
|
1568
1673
|
let out = Array.isArray(value) ? value.map(String) : String(value);
|
|
1569
1674
|
if (name === "content-type") {
|
|
@@ -1592,7 +1697,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1592
1697
|
* @returns {string|string[]|undefined}
|
|
1593
1698
|
*/
|
|
1594
1699
|
get(field) {
|
|
1595
|
-
const name = field
|
|
1700
|
+
const name = headerKey(field);
|
|
1596
1701
|
const value = this.headers[name];
|
|
1597
1702
|
// Content-Length is uWS's, measured from the body it was handed: worked out here only for
|
|
1598
1703
|
// whoever asks, morgan's common and combined formats do, and kept
|
|
@@ -1631,7 +1736,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1631
1736
|
if (this.headersSent) {
|
|
1632
1737
|
throw headersSentError("remove");
|
|
1633
1738
|
}
|
|
1634
|
-
const key = field
|
|
1739
|
+
const key = headerKey(field);
|
|
1635
1740
|
// helmet removes a header most responses never carry, and a delete is a runtime call
|
|
1636
1741
|
if (key in this.headers) {
|
|
1637
1742
|
delete this.headers[key];
|
package/src/uws.js
CHANGED
|
@@ -21,6 +21,30 @@ limitations under the License.
|
|
|
21
21
|
/** @type {any} */
|
|
22
22
|
let uWS;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A project on pnpm owns the uWebSockets.js dependency itself, see `npx fulmine.js pnpm`, so the
|
|
26
|
+
* one installed can drift from the one this package pins and was tested against. Said once, when
|
|
27
|
+
* it is loaded: an app served through node's http never loads it and has nothing to hear.
|
|
28
|
+
*/
|
|
29
|
+
function warnOnDrift() {
|
|
30
|
+
const pinned = /#v?([\d.]+)$/.exec(require("../package.json").dependencies["uWebSockets.js"])?.[1];
|
|
31
|
+
/** @type {string|undefined} */
|
|
32
|
+
let installed;
|
|
33
|
+
try {
|
|
34
|
+
// its exports map does not expose package.json, so it is read beside the entry point
|
|
35
|
+
const beside = require("path").join(require.resolve("uWebSockets.js"), "..", "package.json");
|
|
36
|
+
installed = JSON.parse(require("fs").readFileSync(beside, "utf8")).version;
|
|
37
|
+
} catch {
|
|
38
|
+
// nothing to compare against, which is not worth a warning of its own
|
|
39
|
+
}
|
|
40
|
+
if (pinned && installed && installed !== pinned) {
|
|
41
|
+
console.warn(
|
|
42
|
+
`fulmine.js: uWebSockets.js ${installed} is installed, this version was tested with ${pinned}.\n` +
|
|
43
|
+
" On pnpm the pin is the project's own: `npx fulmine.js pnpm` writes the tested one."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
/**
|
|
25
49
|
* The µWS module. H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the
|
|
26
50
|
* .d.ts the package ships, so it is handed back loosely typed.
|
|
@@ -29,6 +53,7 @@ let uWS;
|
|
|
29
53
|
*/
|
|
30
54
|
function loadUWS() {
|
|
31
55
|
if (uWS === undefined) {
|
|
56
|
+
warnOnDrift();
|
|
32
57
|
uWS = /** @type {any} */ (require("uWebSockets.js"));
|
|
33
58
|
try {
|
|
34
59
|
// disable Uwebsockets header
|