fulmine.js 5.1.3 → 5.1.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 +3 -2
- package/src/application.js +5 -2
- package/src/middlewares.js +3 -2
- package/src/node-shim.js +2 -0
- package/src/parse-query.js +120 -0
- package/src/request.js +9 -4
- package/src/router.js +17 -1
- package/src/utils.js +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.4",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"cookie": "^1.1.1",
|
|
71
71
|
"cookie-signature": "^1.2.2",
|
|
72
72
|
"encodeurl": "^2.0.0",
|
|
73
|
-
"fast-
|
|
73
|
+
"fast-decode-uri-component": "^1.0.1",
|
|
74
74
|
"fast-zlib": "^2.0.1",
|
|
75
75
|
"fresh": "^2.0.0",
|
|
76
76
|
"iconv-lite": "^0.7.3",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"express-rate-limit": "^8.5.2",
|
|
132
132
|
"express-session": "^1.19.0",
|
|
133
133
|
"express-subdomain": "^1.0.6",
|
|
134
|
+
"fast-querystring": "^1.1.2",
|
|
134
135
|
"globals": "^17.8.0",
|
|
135
136
|
"graphql-http": "^1.22.4",
|
|
136
137
|
"helmet": "^8.2.0",
|
package/src/application.js
CHANGED
|
@@ -28,7 +28,7 @@ const {
|
|
|
28
28
|
fastQueryParse,
|
|
29
29
|
NullObject
|
|
30
30
|
} = require("./utils.js");
|
|
31
|
-
const
|
|
31
|
+
const parseQuery = require("./parse-query.js");
|
|
32
32
|
const Request = require("./request.js");
|
|
33
33
|
const Response = require("./response.js");
|
|
34
34
|
const ViewClass = require("./view.js");
|
|
@@ -345,7 +345,7 @@ class Application extends Router {
|
|
|
345
345
|
if (value === "extended") {
|
|
346
346
|
this.settings["query parser fn"] = fastQueryParse;
|
|
347
347
|
} else if (value === "simple" || value === true) {
|
|
348
|
-
this.settings["query parser fn"] =
|
|
348
|
+
this.settings["query parser fn"] = parseQuery;
|
|
349
349
|
} else if (typeof value === "function") {
|
|
350
350
|
this.settings["query parser fn"] = value;
|
|
351
351
|
} else if (value === false) {
|
|
@@ -460,6 +460,9 @@ class Application extends Router {
|
|
|
460
460
|
this.uwsApp.any("/*", async (res, req) => {
|
|
461
461
|
const request = this.handleRequest(res, req);
|
|
462
462
|
const response = request.res;
|
|
463
|
+
// armed up front here: this handler awaits, so the response outlives the callback
|
|
464
|
+
// on every path through it
|
|
465
|
+
this._armAbort(res, response);
|
|
463
466
|
|
|
464
467
|
try {
|
|
465
468
|
const routed = this._routeRequest(request, response);
|
package/src/middlewares.js
CHANGED
|
@@ -21,7 +21,7 @@ const bytes = require("bytes");
|
|
|
21
21
|
const zlib = require("fast-zlib");
|
|
22
22
|
const typeis = require("type-is");
|
|
23
23
|
const qs = require("qs");
|
|
24
|
-
const
|
|
24
|
+
const parseQuery = require("./parse-query.js");
|
|
25
25
|
const { AsyncResource } = require("async_hooks");
|
|
26
26
|
const { fastQueryParse, NullObject, asStatError, httpError, memoizeByString } = require("./utils.js");
|
|
27
27
|
|
|
@@ -954,7 +954,8 @@ const urlencoded = createBodyParser(
|
|
|
954
954
|
})
|
|
955
955
|
);
|
|
956
956
|
} else {
|
|
957
|
-
|
|
957
|
+
// the vendored parser, so an urlencoded body inspects like req.query does
|
|
958
|
+
req.body = parseQuery(body);
|
|
958
959
|
}
|
|
959
960
|
} catch (e) {
|
|
960
961
|
// qs reports a depth overflow as a RangeError with its own wording; body-parser
|
package/src/node-shim.js
CHANGED
|
@@ -395,6 +395,8 @@ function serveNodeRequest(router, nodeReq, nodeRes, next) {
|
|
|
395
395
|
const shimReq = new NodeHttpRequest(nodeReq);
|
|
396
396
|
const request = router.handleRequest(shimRes, shimReq);
|
|
397
397
|
const response = request.res;
|
|
398
|
+
// the shim's onAborted rides node's own close event, needed on every request here
|
|
399
|
+
router._armAbort(shimRes, response);
|
|
398
400
|
|
|
399
401
|
return router._routeRequest(request, response).then((matched) => {
|
|
400
402
|
if (matched || response.headersSent || response.aborted) {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The parser from fast-querystring 1.1.x (MIT, Copyright (c) Yagiz Nizipli,
|
|
5
|
+
https://github.com/anonrig/fast-querystring), vendored for one change: the result is a bare
|
|
6
|
+
Object.create(null) instead of the library's Empty-constructor trick. The trick is faster to
|
|
7
|
+
construct but node inspects it as "Empty <[Object: null prototype] {}>", where Express shows
|
|
8
|
+
"[Object: null prototype]", and matching that used to cost an Object.assign copy of every parse
|
|
9
|
+
on every query-carrying request. The copy was worth more than the trick.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fastDecode = require("fast-decode-uri-component");
|
|
13
|
+
|
|
14
|
+
const plusRegex = /\+/g;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* node's querystring.parse semantics on a null-prototype result: repeated keys accumulate into
|
|
18
|
+
* arrays, '+' is a space, percent sequences decode when present and stay literal when broken.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} input
|
|
21
|
+
* @returns {Record<string, string | string[]>}
|
|
22
|
+
*/
|
|
23
|
+
function parseQuery(input) {
|
|
24
|
+
const result = Object.create(null);
|
|
25
|
+
|
|
26
|
+
if (typeof input !== "string") {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const inputLength = input.length;
|
|
31
|
+
let key;
|
|
32
|
+
let value = "";
|
|
33
|
+
let startingIndex = -1;
|
|
34
|
+
let equalityIndex = -1;
|
|
35
|
+
let shouldDecodeKey = false;
|
|
36
|
+
let shouldDecodeValue = false;
|
|
37
|
+
let keyHasPlus = false;
|
|
38
|
+
let valueHasPlus = false;
|
|
39
|
+
let hasBothKeyValuePair;
|
|
40
|
+
let c;
|
|
41
|
+
|
|
42
|
+
// a boundary of input.length + 1, so the last pair is handled inside the loop
|
|
43
|
+
for (let i = 0; i < inputLength + 1; i++) {
|
|
44
|
+
c = i !== inputLength ? input.charCodeAt(i) : 38;
|
|
45
|
+
|
|
46
|
+
// '&' or the end of the input closes the current pair
|
|
47
|
+
if (c === 38) {
|
|
48
|
+
hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
49
|
+
|
|
50
|
+
// the equality index doubles as the end of the key when there was no '='
|
|
51
|
+
if (!hasBothKeyValuePair) {
|
|
52
|
+
equalityIndex = i;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
key = input.slice(startingIndex + 1, equalityIndex);
|
|
56
|
+
|
|
57
|
+
// only a pair with at least an '=' or a non-empty key lands in the result
|
|
58
|
+
if (hasBothKeyValuePair || key.length > 0) {
|
|
59
|
+
if (keyHasPlus) {
|
|
60
|
+
key = key.replace(plusRegex, " ");
|
|
61
|
+
}
|
|
62
|
+
if (shouldDecodeKey) {
|
|
63
|
+
key = fastDecode(key) || key;
|
|
64
|
+
}
|
|
65
|
+
if (hasBothKeyValuePair) {
|
|
66
|
+
value = input.slice(equalityIndex + 1, i);
|
|
67
|
+
if (valueHasPlus) {
|
|
68
|
+
value = value.replace(plusRegex, " ");
|
|
69
|
+
}
|
|
70
|
+
if (shouldDecodeValue) {
|
|
71
|
+
value = fastDecode(value) || value;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const currentValue = result[key];
|
|
76
|
+
if (currentValue === undefined) {
|
|
77
|
+
result[key] = value;
|
|
78
|
+
} else {
|
|
79
|
+
// value.pop is cheaper than Array.isArray here, as upstream measured
|
|
80
|
+
if (currentValue.pop) {
|
|
81
|
+
currentValue.push(value);
|
|
82
|
+
} else {
|
|
83
|
+
result[key] = [currentValue, value];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
value = "";
|
|
89
|
+
startingIndex = i;
|
|
90
|
+
equalityIndex = i;
|
|
91
|
+
shouldDecodeKey = false;
|
|
92
|
+
shouldDecodeValue = false;
|
|
93
|
+
keyHasPlus = false;
|
|
94
|
+
valueHasPlus = false;
|
|
95
|
+
} else if (c === 61) {
|
|
96
|
+
if (equalityIndex <= startingIndex) {
|
|
97
|
+
equalityIndex = i;
|
|
98
|
+
} else {
|
|
99
|
+
// a second '=' belongs to the value and needs decoding
|
|
100
|
+
shouldDecodeValue = true;
|
|
101
|
+
}
|
|
102
|
+
} else if (c === 43) {
|
|
103
|
+
if (equalityIndex > startingIndex) {
|
|
104
|
+
valueHasPlus = true;
|
|
105
|
+
} else {
|
|
106
|
+
keyHasPlus = true;
|
|
107
|
+
}
|
|
108
|
+
} else if (c === 37) {
|
|
109
|
+
if (equalityIndex > startingIndex) {
|
|
110
|
+
shouldDecodeValue = true;
|
|
111
|
+
} else {
|
|
112
|
+
shouldDecodeKey = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = parseQuery;
|
package/src/request.js
CHANGED
|
@@ -22,6 +22,7 @@ const parseRange = require("range-parser");
|
|
|
22
22
|
const proxyaddr = require("proxy-addr");
|
|
23
23
|
const { isIP } = require("node:net");
|
|
24
24
|
const fresh = require("fresh");
|
|
25
|
+
const parseQuery = require("./parse-query.js");
|
|
25
26
|
const { Readable } = require("stream");
|
|
26
27
|
|
|
27
28
|
// accepts, type-is, proxy-addr and fresh all declare a node IncomingMessage and read nothing off
|
|
@@ -540,10 +541,14 @@ module.exports = class Request extends Readable {
|
|
|
540
541
|
return this.#cachedQuery;
|
|
541
542
|
}
|
|
542
543
|
const qp = this.app.get("query parser fn");
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
const parsed = qp
|
|
544
|
+
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
545
|
+
// any other parser is copied onto one, which is what kept fast-querystring's result from
|
|
546
|
+
// inspecting as "Empty <[Object: null prototype] {}>" where Express shows the bare form
|
|
547
|
+
const parsed = qp
|
|
548
|
+
? qp === parseQuery
|
|
549
|
+
? parseQuery(this._rawQuery)
|
|
550
|
+
: Object.assign(Object.create(null), qp(this._rawQuery))
|
|
551
|
+
: Object.create(null);
|
|
547
552
|
this.#cachedQuery = parsed;
|
|
548
553
|
return parsed;
|
|
549
554
|
}
|
package/src/router.js
CHANGED
|
@@ -1345,11 +1345,22 @@ module.exports = class Router extends EventEmitter {
|
|
|
1345
1345
|
const response = new this._response(res, request, this);
|
|
1346
1346
|
request.res = response;
|
|
1347
1347
|
response.req = request;
|
|
1348
|
-
res.onAborted(onNativeAborted.bind(response));
|
|
1349
1348
|
|
|
1350
1349
|
return request;
|
|
1351
1350
|
}
|
|
1352
1351
|
|
|
1352
|
+
/**
|
|
1353
|
+
* Tells uWS whom to call on a client abort. Out of handleRequest, because uWS only needs it
|
|
1354
|
+
* for a response that outlives its handler callback: the native handler arms it in its
|
|
1355
|
+
* finally when the answer is still pending, which on a synchronous route it never is.
|
|
1356
|
+
*
|
|
1357
|
+
* @param {any} res uWS response
|
|
1358
|
+
* @param {any} response
|
|
1359
|
+
*/
|
|
1360
|
+
_armAbort(res, response) {
|
|
1361
|
+
res.onAborted(onNativeAborted.bind(response));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1353
1364
|
/**
|
|
1354
1365
|
* Whether a route registered later in the same router could match a path this one matches.
|
|
1355
1366
|
*
|
|
@@ -1437,6 +1448,11 @@ module.exports = class Router extends EventEmitter {
|
|
|
1437
1448
|
// whatever runs after this line is outside the cork uWS held for this
|
|
1438
1449
|
// callback, so later writes have to open their own
|
|
1439
1450
|
response._corkNeeded = true;
|
|
1451
|
+
// an abort can only arrive after this callback returns, so a response that
|
|
1452
|
+
// already finished inside it never needs uWS told at all
|
|
1453
|
+
if (!response.finished) {
|
|
1454
|
+
this._armAbort(res, response);
|
|
1455
|
+
}
|
|
1440
1456
|
}
|
|
1441
1457
|
};
|
|
1442
1458
|
};
|
package/src/utils.js
CHANGED
|
@@ -19,7 +19,7 @@ const mime = require("mime-types");
|
|
|
19
19
|
const path = require("path");
|
|
20
20
|
const proxyaddr = require("proxy-addr");
|
|
21
21
|
const qs = require("qs");
|
|
22
|
-
const
|
|
22
|
+
const parseQuery = require("./parse-query.js");
|
|
23
23
|
const crypto = require("crypto");
|
|
24
24
|
const statuses = require("statuses");
|
|
25
25
|
const { Stats } = require("fs");
|
|
@@ -46,7 +46,8 @@ function fastQueryParse(query, options) {
|
|
|
46
46
|
}
|
|
47
47
|
if (len <= 128) {
|
|
48
48
|
if (!query.includes("[") && !query.includes("%5B") && !query.includes(".") && !query.includes("%2E")) {
|
|
49
|
-
|
|
49
|
+
// already on a bare null prototype, no copy needed, see parse-query.js
|
|
50
|
+
return parseQuery(query);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
return Object.assign(Object.create(null), qs.parse(query, options));
|
|
@@ -564,7 +565,7 @@ const defaultSettings = {
|
|
|
564
565
|
etag: "weak",
|
|
565
566
|
"etag fn": () => createETagGenerator({ weak: true }),
|
|
566
567
|
"query parser": "simple",
|
|
567
|
-
"query parser fn": () =>
|
|
568
|
+
"query parser fn": () => parseQuery,
|
|
568
569
|
"subdomain offset": 2,
|
|
569
570
|
"trust proxy": false,
|
|
570
571
|
views: () => path.join(process.cwd(), "views"),
|