fulmine.js 5.1.7 → 5.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.1.7",
3
+ "version": "5.1.8",
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": {
@@ -365,6 +365,7 @@ class Application extends Router {
365
365
  if (value !== false && this._skipPresets?.size) {
366
366
  for (const preset of this._skipPresets) {
367
367
  preset.skipHeaders = false;
368
+ preset.skipQuery = false;
368
369
  }
369
370
  this._skipPresets.clear();
370
371
  }
package/src/request.js CHANGED
@@ -263,9 +263,16 @@ module.exports = class Request extends Readable {
263
263
  this.app = app;
264
264
  // both forms are kept, because both are asked for: the query with its "?" goes into
265
265
  // req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
266
- // back off for every request that reads req.query.
267
- this._rawQuery = req.getQuery() ?? "";
268
- this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
266
+ // back off for every request that reads req.query. When the chain provably reads
267
+ // neither, the native call is not made at all: the framework's own answers, the 404
268
+ // included, are written from the path alone
269
+ if (skipHolder !== undefined && skipHolder.skipQuery) {
270
+ this._rawQuery = "";
271
+ this.urlQuery = "";
272
+ } else {
273
+ this._rawQuery = req.getQuery() ?? "";
274
+ this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
275
+ }
269
276
  if (preset) {
270
277
  // the registration's constants: two native crossings and their strings not asked for
271
278
  this.path = preset.path;
package/src/router.js CHANGED
@@ -36,7 +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
+ const { chainUsage } = require("./usage.js");
40
40
 
41
41
  // every method the declarative compiler can emit: a patched one must disable compilation, or the
42
42
  // patch would be honoured everywhere but on compiled routes
@@ -693,9 +693,10 @@ function nativePreset(path, method, strict) {
693
693
  opPath: endsWithSlash && path !== "/" && !strict ? path.slice(0, -1) : path,
694
694
  isOptions: method === "OPTIONS",
695
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
696
+ // set at registration when the whole chain provably never reads a header, or never
697
+ // reads the query; mutable, because a middleware added after listen takes them back
698
+ skipHeaders: false,
699
+ skipQuery: false
699
700
  };
700
701
  }
701
702
 
@@ -1148,6 +1149,7 @@ module.exports = class Router extends EventEmitter {
1148
1149
  if (this._skipPresets?.size) {
1149
1150
  for (const preset of this._skipPresets) {
1150
1151
  preset.skipHeaders = false;
1152
+ preset.skipQuery = false;
1151
1153
  }
1152
1154
  this._skipPresets.clear();
1153
1155
  }
@@ -1469,8 +1471,8 @@ module.exports = class Router extends EventEmitter {
1469
1471
  // listen can take it back: a literal registration's preset doubles as it, and a
1470
1472
  // parameterised one, which has no preset, gets a holder of its own
1471
1473
  let skipHolder = preset;
1472
- if (skipHolder === undefined && skips) {
1473
- skipHolder = { skipHeaders: true };
1474
+ if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
1475
+ skipHolder = { skipHeaders: skips.skipHeaders, skipQuery: skips.skipQuery };
1474
1476
  (this._skipPresets ??= new Set()).add(skipHolder);
1475
1477
  }
1476
1478
  // all three are registration-time constants: computing them in the handler was a
@@ -1532,8 +1534,9 @@ module.exports = class Router extends EventEmitter {
1532
1534
  // headers), no error middleware may exist anywhere (a throw hands the request to code
1533
1535
  // the analysis never saw), and every callback in the chain has to pass the source
1534
1536
  // analysis in usage.js, whose default answer is no.
1535
- let getSkips = false;
1536
- let headSkips = false;
1537
+ const NO_SKIPS = { skipHeaders: false, skipQuery: false };
1538
+ let getSkips = NO_SKIPS;
1539
+ let headSkips = NO_SKIPS;
1537
1540
  if (route.method === "GET" && this.get("etag") === false) {
1538
1541
  let hasErr = this._hasErrMwCache;
1539
1542
  if (hasErr === undefined) {
@@ -1544,15 +1547,16 @@ module.exports = class Router extends EventEmitter {
1544
1547
  // route may be able to catch the same path
1545
1548
  const owner = route.owner ?? this;
1546
1549
  const noLaterMatch = !owner._isFollowedByAnOverlap.call(owner, route, owner._routes);
1547
- getSkips = chainSkipsHeaders(getChain, noLaterMatch);
1548
- headSkips = headChain === getChain ? getSkips : chainSkipsHeaders(headChain, noLaterMatch);
1550
+ getSkips = chainUsage(getChain, noLaterMatch);
1551
+ headSkips = headChain === getChain ? getSkips : chainUsage(headChain, noLaterMatch);
1549
1552
  }
1550
1553
  }
1551
1554
  // remembered so a middleware or setting arriving after listen can take the skips back
1552
1555
  const makePreset = (path, method, skips) => {
1553
1556
  const preset = nativePreset(path, method, strictHere);
1554
- if (skips) {
1555
- preset.skipHeaders = true;
1557
+ if (skips.skipHeaders || skips.skipQuery) {
1558
+ preset.skipHeaders = skips.skipHeaders;
1559
+ preset.skipQuery = skips.skipQuery;
1556
1560
  (this._skipPresets ??= new Set()).add(preset);
1557
1561
  }
1558
1562
  return preset;
package/src/usage.js CHANGED
@@ -13,6 +13,13 @@ const kGetSafe = Symbol("fulmine.getSafe");
13
13
  // the parameter, so anything that can reach another object could reach headers through it.
14
14
  const REQ_OK = new Set(["query", "params", "body", "method", "path", "url", "baseUrl", "originalUrl", "route"]);
15
15
 
16
+ // Reading any of these needs the query string fetched: req.url and req.originalUrl carry it
17
+ const REQ_QUERY = new Set(["query", "url", "originalUrl"]);
18
+
19
+ // Writing any of these re-enters routing: dispatch treats a changed req.url as a rewrite and
20
+ // walks routes nobody analyzed, so an assignment is as disqualifying as an unknown call
21
+ const REQ_NO_WRITE = new Set(["url", "originalUrl", "path", "baseUrl", "method"]);
22
+
16
23
  // What a handler may do with `res`: writing the response. Anything that negotiates against
17
24
  // request headers (format, redirect, sendFile, jsonp) is deliberately absent.
18
25
  const RES_OK = new Set([
@@ -38,25 +45,26 @@ const RES_OK = new Set([
38
45
  "cork"
39
46
  ]);
40
47
 
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
48
+ // what the analysis can say about one callback, as independent facts
49
+ const UNKNOWN = 1; // a shape the walk cannot vouch for: could do anything
50
+ const NEXT_PLAIN = 2; // calls next() bare: advances the chain, may fall off its end
51
+ const NEXT_ERROR = 4; // calls next(err): lands in the framework's own error answer
52
+ const QUERY = 8; // reads req.query, req.url or req.originalUrl
46
53
 
47
54
  const verdicts = new WeakMap();
48
55
 
49
56
  /**
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.
57
+ * What one callback provably does, as a mask of the facts above. The default is UNKNOWN: any
58
+ * shape this walk does not understand and any alias of req, res or next could do anything.
59
+ * That inversion is what makes source analysis sound to act on.
53
60
  *
54
61
  * @param {Function} fn
55
- * @returns {number} NO, SAFE or SAFE_NEXT
62
+ * @returns {number}
56
63
  */
57
- function callbackSkipsHeaders(fn) {
64
+ function callbackUsage(fn) {
58
65
  if (fn[kGetSafe]) {
59
- return SAFE_NEXT;
66
+ // the body parsers: they advance the chain and read nothing a skip would miss
67
+ return NEXT_PLAIN;
60
68
  }
61
69
  let verdict = verdicts.get(fn);
62
70
  if (verdict === undefined) {
@@ -77,11 +85,11 @@ function analyze(fn) {
77
85
  tree = acorn.parse(code, { ecmaVersion: "latest" });
78
86
  } catch {
79
87
  // class methods and native functions do not parse alone, and unread code is unknown code
80
- return NO;
88
+ return UNKNOWN;
81
89
  }
82
90
  let root = /** @type {any} */ (tree.body[0]);
83
91
  if (!root) {
84
- return NO;
92
+ return UNKNOWN;
85
93
  }
86
94
  if (root.type === "ExpressionStatement") {
87
95
  root = root.expression;
@@ -91,14 +99,14 @@ function analyze(fn) {
91
99
  root.type !== "ArrowFunctionExpression" &&
92
100
  root.type !== "FunctionExpression"
93
101
  ) {
94
- return NO;
102
+ return UNKNOWN;
95
103
  }
96
104
 
97
105
  const params = /** @type {any[]} */ (root.params);
98
106
  // rest or destructured parameters alias the objects somewhere the walk cannot follow
99
107
  for (const p of params) {
100
108
  if (p.type !== "Identifier") {
101
- return NO;
109
+ return UNKNOWN;
102
110
  }
103
111
  }
104
112
  const reqName = params[0] ? params[0].name : null;
@@ -108,15 +116,38 @@ function analyze(fn) {
108
116
  // Every appearance of the three names in the whole body is judged, nested functions
109
117
  // included: an inner binding that shadows one of them only makes this stricter, never
110
118
  // looser, so scope tracking is not needed for soundness.
111
- let ok = true;
112
- let usesNext = false;
119
+ let mask = 0;
113
120
  walk(root.body, null, (node, parent) => {
114
- if (!ok || node.type !== "Identifier") {
121
+ if (mask & UNKNOWN) {
122
+ return;
123
+ }
124
+ if (node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier") {
125
+ const owner = node.object.name;
126
+ if (owner !== reqName) {
127
+ return;
128
+ }
129
+ const member = node.property.name;
130
+ if (REQ_QUERY.has(member)) {
131
+ mask |= QUERY;
132
+ }
133
+ // an assignment, an update or a delete on the routing members re-enters dispatch
134
+ if (
135
+ REQ_NO_WRITE.has(member) &&
136
+ parent &&
137
+ ((parent.type === "AssignmentExpression" && parent.left === node) ||
138
+ (parent.type === "UpdateExpression" && parent.argument === node) ||
139
+ (parent.type === "UnaryExpression" && parent.operator === "delete" && parent.argument === node))
140
+ ) {
141
+ mask |= UNKNOWN;
142
+ }
143
+ return;
144
+ }
145
+ if (node.type !== "Identifier") {
115
146
  return;
116
147
  }
117
148
  const name = node.name;
118
149
  if (name === "eval" || name === "arguments") {
119
- ok = false;
150
+ mask |= UNKNOWN;
120
151
  return;
121
152
  }
122
153
  if (name !== reqName && name !== resName && name !== nextName) {
@@ -140,17 +171,17 @@ function analyze(fn) {
140
171
  }
141
172
  if (name === nextName) {
142
173
  // 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
174
+ // request lands in the framework's own final answer, which the constructor's
144
175
  // accept pre-read covers. Anything but a direct call aliases the continuation,
145
176
  // and an argument that could be the string "route" would leave the chain for
146
177
  // routes nobody analyzed, so only shapes that cannot be a string pass.
147
178
  if (!parent || parent.type !== "CallExpression" || parent.callee !== node) {
148
- ok = false;
179
+ mask |= UNKNOWN;
149
180
  return;
150
181
  }
151
- usesNext = true;
152
182
  const args = parent.arguments;
153
183
  if (args.length === 0) {
184
+ mask |= NEXT_PLAIN;
154
185
  return;
155
186
  }
156
187
  const arg = args[0];
@@ -160,20 +191,22 @@ function analyze(fn) {
160
191
  arg.type !== "ObjectExpression" &&
161
192
  !(arg.type === "Literal" && typeof arg.value !== "string"))
162
193
  ) {
163
- ok = false;
194
+ mask |= UNKNOWN;
195
+ return;
164
196
  }
197
+ mask |= NEXT_ERROR;
165
198
  return;
166
199
  }
167
200
  if (!parent || parent.type !== "MemberExpression" || parent.object !== node || parent.computed) {
168
- ok = false;
201
+ mask |= UNKNOWN;
169
202
  return;
170
203
  }
171
204
  const member = parent.property.name;
172
205
  if (name === reqName ? !REQ_OK.has(member) : !RES_OK.has(member)) {
173
- ok = false;
206
+ mask |= UNKNOWN;
174
207
  }
175
208
  });
176
- return ok ? (usesNext ? SAFE_NEXT : SAFE) : NO;
209
+ return mask;
177
210
  }
178
211
 
179
212
  /**
@@ -207,41 +240,47 @@ function walk(node, parent, visit) {
207
240
  }
208
241
 
209
242
  /**
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.
243
+ * What a native route's whole chain provably never does, so the request constructor may leave
244
+ * that work undone: skipHeaders spares the header copy, skipQuery the query fetch.
212
245
  *
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.
246
+ * A callback that calls next() bare passes anywhere but in the terminal route, where it would
247
+ * fall out of the chain: there it only passes when the caller established that no later route
248
+ * could catch the fall-through. The framework's own 404 answers with the path alone, so the
249
+ * fall-through itself needs neither headers nor query.
216
250
  *
217
251
  * @param {any[]} chain the routes the native handler runs, in order, this route last
218
252
  * @param {boolean} allowTerminalNext whether a fall-through past the chain lands only in the
219
- * framework's own final handler
220
- * @returns {boolean}
253
+ * framework's own final answer
254
+ * @returns {{skipHeaders: boolean, skipQuery: boolean}}
221
255
  */
222
- function chainSkipsHeaders(chain, allowTerminalNext) {
256
+ function chainUsage(chain, allowTerminalNext) {
257
+ const none = { skipHeaders: false, skipQuery: false };
258
+ let query = false;
223
259
  for (let i = 0; i < chain.length; i++) {
224
260
  const entry = chain[i];
225
261
  const callbacks = entry.callbacks;
226
262
  if (!Array.isArray(callbacks)) {
227
- return false;
263
+ return none;
228
264
  }
229
265
  const terminal = i === chain.length - 1;
230
266
  for (const cb of callbacks) {
231
267
  if (typeof cb !== "function") {
232
- return false;
268
+ return none;
269
+ }
270
+ const mask = callbackUsage(cb);
271
+ if (mask & UNKNOWN || (mask & NEXT_PLAIN && terminal && !allowTerminalNext)) {
272
+ return none;
233
273
  }
234
- const verdict = callbackSkipsHeaders(cb);
235
- if (verdict === NO || (verdict === SAFE_NEXT && terminal && !allowTerminalNext)) {
236
- return false;
274
+ if (mask & QUERY) {
275
+ query = true;
237
276
  }
238
277
  }
239
278
  // a param callback runs code this walk never saw
240
279
  if (entry.paramCallbacks && entry.paramCallbacks.size > 0) {
241
- return false;
280
+ return none;
242
281
  }
243
282
  }
244
- return true;
283
+ return { skipHeaders: true, skipQuery: !query };
245
284
  }
246
285
 
247
- module.exports = { chainSkipsHeaders, callbackSkipsHeaders, kGetSafe };
286
+ module.exports = { chainUsage, callbackUsage, kGetSafe, UNKNOWN, NEXT_PLAIN, NEXT_ERROR, QUERY };