fulmine.js 5.19.2 → 5.19.3
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 +46 -45
- package/src/cli.js +28 -34
- package/src/cluster.js +16 -25
- package/src/compression.js +39 -51
- package/src/declarative.js +586 -538
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +129 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +61 -77
- package/src/nest.js +19 -34
- package/src/node-shim.js +11 -13
- package/src/optimizer.js +598 -0
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +306 -0
- package/src/request.js +101 -513
- package/src/response-utils.js +88 -0
- package/src/response.js +100 -443
- package/src/route.js +4 -5
- package/src/router-utils.js +950 -0
- package/src/router.js +126 -2148
- package/src/server-shape.js +26 -41
- package/src/server-timing.js +16 -29
- package/src/socket.js +208 -0
- package/src/testing.js +39 -42
- package/src/usage.js +16 -21
- package/src/utils.js +49 -59
- package/src/verify.js +18 -28
- package/src/view.js +5 -7
- package/src/walk.js +580 -0
- package/src/websocket.js +19 -20
- package/src/work.js +21 -27
package/src/walk.js
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
mountPrefixLength,
|
|
22
|
+
setMountedPath,
|
|
23
|
+
stepsOver,
|
|
24
|
+
rememberApp,
|
|
25
|
+
restoreApp,
|
|
26
|
+
useApp,
|
|
27
|
+
CALLBACK_ERROR,
|
|
28
|
+
CALLBACK_ROUTER
|
|
29
|
+
} = require("./router-utils.js");
|
|
30
|
+
|
|
31
|
+
/** @typedef {import("./request.js")} Request */
|
|
32
|
+
/** @typedef {import("./response.js")} Response */
|
|
33
|
+
/** @typedef {import("./router.js")} Router */
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One walk of one router's routes, for one request.
|
|
37
|
+
*
|
|
38
|
+
* next() is made once here instead of once per hop. As a closure per hop it captured eleven
|
|
39
|
+
* bindings, one of them mutable, so a heap context every time a middleware handed over. The hop's
|
|
40
|
+
* own state is three fields on this instead.
|
|
41
|
+
*
|
|
42
|
+
* A nested router gets its own walk, so req.next belongs to whoever is running the request.
|
|
43
|
+
*/
|
|
44
|
+
class Walk {
|
|
45
|
+
/**
|
|
46
|
+
* @param {Router} router
|
|
47
|
+
* @param {Request} req
|
|
48
|
+
* @param {Response} res
|
|
49
|
+
* @param {any[]} routes the route table being walked, see createRoute in router.js
|
|
50
|
+
* @param {boolean} skipCheck take the route at the index without matching it, which is how an
|
|
51
|
+
* already-decided chain is walked
|
|
52
|
+
* @param {any} skipUntil route to resume after when this chain runs out, or undefined
|
|
53
|
+
* @param {(value: any) => void} resolve
|
|
54
|
+
* @param {(err: any) => void} reject
|
|
55
|
+
*/
|
|
56
|
+
constructor(router, req, res, routes, skipCheck, skipUntil, resolve, reject) {
|
|
57
|
+
this.router = router;
|
|
58
|
+
this.req = req;
|
|
59
|
+
this.res = res;
|
|
60
|
+
this.routes = routes;
|
|
61
|
+
this.skipCheck = skipCheck;
|
|
62
|
+
this.skipUntil = skipUntil;
|
|
63
|
+
this.resolve = resolve;
|
|
64
|
+
this.reject = reject;
|
|
65
|
+
// read only by the native pair below, which has no promise to settle once for it; the
|
|
66
|
+
// promise path leaves it false. Initialized here to keep every walk the same shape
|
|
67
|
+
this.settled = false;
|
|
68
|
+
this.routeIndex = 0;
|
|
69
|
+
this.route = null;
|
|
70
|
+
this.callbackIndex = 0;
|
|
71
|
+
// bound, not wrapped in an arrow: an arrow forwarding into step() is one more call on every
|
|
72
|
+
// hop, and it measured 495 microseconds per thousand requests of nothing else
|
|
73
|
+
this.next = this.step.bind(this);
|
|
74
|
+
// What res.sendFile reports a failure to. Express hands it req.next, the router next and
|
|
75
|
+
// not the route one, so a file that cannot be served leaves the route and its error reaches
|
|
76
|
+
// the router error handlers. req.next itself is left alone: making it mean this everywhere
|
|
77
|
+
// breaks express own res.format and app.routes.error tests here.
|
|
78
|
+
//
|
|
79
|
+
// Null here and bound on the first route with more than one callback, the only shape that
|
|
80
|
+
// reads it: a request that never meets one paid a bind for nothing
|
|
81
|
+
this.leaveRoute = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Leaves the rest of this route, with the error if there is one, and carries on with the route
|
|
86
|
+
* after it.
|
|
87
|
+
*
|
|
88
|
+
* @param {any} [err] whatever was thrown, which need not be an Error
|
|
89
|
+
*/
|
|
90
|
+
stepOutOfRoute(err) {
|
|
91
|
+
if (err) {
|
|
92
|
+
const req = this.req;
|
|
93
|
+
req._error = err;
|
|
94
|
+
req._errorKey = this.route.routeKey;
|
|
95
|
+
req._errorGroup = this.route.group;
|
|
96
|
+
}
|
|
97
|
+
this.step("route");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Finds the next route that matches and runs it. next() comes back here for the route after, so
|
|
102
|
+
* a chain of N middlewares costs one promise instead of N nested ones.
|
|
103
|
+
*
|
|
104
|
+
* @param {number} startIndex where to resume the scan
|
|
105
|
+
*/
|
|
106
|
+
dispatch(startIndex) {
|
|
107
|
+
const req = this.req;
|
|
108
|
+
const routes = this.routes;
|
|
109
|
+
const router = this.router;
|
|
110
|
+
// a middleware assigned req.url, which express honours: the rest of the walk matches the
|
|
111
|
+
// new path. One identity compare per hop, since the router writes both sides itself; the
|
|
112
|
+
// handling lives out of line so this function stays small enough to inline
|
|
113
|
+
if (req.url !== req._lastUrl && this.takeUrlRewrite(startIndex)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// and the same for req.method, which method-override assigns: the compiled chain was
|
|
117
|
+
// picked by the verb the request arrived with, so it no longer stands for this one
|
|
118
|
+
if (req.method !== req._lastMethod && this.takeMethodRewrite(startIndex)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let routeIndex = startIndex;
|
|
122
|
+
// a compiled chain runs what is in it without matching again, so this is where a layer that
|
|
123
|
+
// provably has nothing to do for this request is stepped over rather than entered
|
|
124
|
+
if (this.skipCheck) {
|
|
125
|
+
while (routeIndex < routes.length && routes[routeIndex].bodyParserOnly === true) {
|
|
126
|
+
if (!stepsOver(routes[routeIndex], req)) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
routeIndex++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!this.skipCheck) {
|
|
133
|
+
// express matches a layer's path before the method and decodes the parameters there, so
|
|
134
|
+
// a malformed escape answers 400 even when no route of this method exists. Only a path
|
|
135
|
+
// with a percent can produce one. Scanned once per rewrite and kept on the request
|
|
136
|
+
const mayFailDecode = (req._mayFailDecode ??= req._originalPath.indexOf("%") !== -1);
|
|
137
|
+
// frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
|
|
138
|
+
// calling this per route measured 0.45us of a scan of four hundred
|
|
139
|
+
router._freezeRoutingFlags();
|
|
140
|
+
if (routes === router._routes) {
|
|
141
|
+
// the router's own table has an index over its literal routes, so the scan visits
|
|
142
|
+
// the handful that could match instead of every one, see _scanFrom
|
|
143
|
+
routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
|
|
144
|
+
} else {
|
|
145
|
+
// a compiled chain's own array, always short: the linear scan stays.
|
|
146
|
+
// Written out rather than through a predicate handed to findIndexStartingFrom,
|
|
147
|
+
// which was one closure per hop of every request not on a compiled chain
|
|
148
|
+
const method = req.method;
|
|
149
|
+
const length = routes.length;
|
|
150
|
+
for (; routeIndex < length; routeIndex++) {
|
|
151
|
+
const r = routes[routeIndex];
|
|
152
|
+
// A HEAD request enters a route whose path matched even when its verb cannot
|
|
153
|
+
// serve one: express exempts HEAD from the method check ("if (!hasMethod &&
|
|
154
|
+
// method !== 'HEAD')" in router/index.js), so the layer's parameters are
|
|
155
|
+
// captured and its param() callbacks run first. Only asked when the router has
|
|
156
|
+
// callbacks to run. runRoute steps over it.
|
|
157
|
+
if (!(
|
|
158
|
+
r.all ||
|
|
159
|
+
r.method === method ||
|
|
160
|
+
req._isOptions ||
|
|
161
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
162
|
+
)) {
|
|
163
|
+
// taken only to fail: _preprocessRequest decodes again and turns it into
|
|
164
|
+
// the error, so the handlers of a route this request cannot run never see it
|
|
165
|
+
if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (router._pathMatches(r, req)) {
|
|
171
|
+
// matched, and then stepped over: a body parser this request gets nothing
|
|
172
|
+
// out of costs a hop and answers with next() at the end of it
|
|
173
|
+
if (r.bodyParserOnly === true && stepsOver(r, req)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const route = routes[routeIndex];
|
|
182
|
+
if (!route) {
|
|
183
|
+
if (!this.skipCheck) {
|
|
184
|
+
// on normal unoptimized routes, if theres no match then there is no route
|
|
185
|
+
return this.resolve(false);
|
|
186
|
+
}
|
|
187
|
+
// the chain ran out, so ordinary routing takes over from the top and skips what has
|
|
188
|
+
// already run
|
|
189
|
+
useApp(req, router);
|
|
190
|
+
// a chain that went into a mount never left it, since keepMount stops the pop, so the
|
|
191
|
+
// path is still relative to it. /alone/skip must not be offered to the app as /skip
|
|
192
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
193
|
+
req._stack.length = 0;
|
|
194
|
+
req._consumed = 0;
|
|
195
|
+
setMountedPath(req);
|
|
196
|
+
}
|
|
197
|
+
// an error out of a mount is attributed to the mount, so error handlers declared before
|
|
198
|
+
// it do not catch it, as in ordinary dispatch
|
|
199
|
+
if (
|
|
200
|
+
req._error &&
|
|
201
|
+
this.skipUntil &&
|
|
202
|
+
this.skipUntil.keepMount &&
|
|
203
|
+
this.skipUntil.routeKey > /** @type {number} */ (req._errorKey)
|
|
204
|
+
) {
|
|
205
|
+
req._errorKey = this.skipUntil.routeKey;
|
|
206
|
+
req._errorGroup = this.skipUntil.group;
|
|
207
|
+
}
|
|
208
|
+
this.routes = router._routes;
|
|
209
|
+
this.skipCheck = false;
|
|
210
|
+
return this.dispatch(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.routeIndex = routeIndex;
|
|
214
|
+
this.route = route;
|
|
215
|
+
this.callbackIndex = 0;
|
|
216
|
+
|
|
217
|
+
// _preprocessRequest returns a promise only when param callbacks will really run, so the
|
|
218
|
+
// common case stays synchronous even in an app that uses app.param. A microtask every 300
|
|
219
|
+
// routes resets the stack, which a long chain would otherwise blow
|
|
220
|
+
const continueRoute = router._preprocessRequest(req, this.res, route);
|
|
221
|
+
if (continueRoute instanceof Promise || req.routeCount % 300 === 0) {
|
|
222
|
+
// .catch and not a rejection argument: a throw inside runRoute itself must reject
|
|
223
|
+
// the walk instead of becoming an unhandled rejection
|
|
224
|
+
Promise.resolve(continueRoute)
|
|
225
|
+
.then((resumed) => this.runRoute(resumed))
|
|
226
|
+
// wrapped so the native pair keeps the walk as receiver; a promise's reject
|
|
227
|
+
// would not have cared
|
|
228
|
+
.catch((err) => this.reject(err));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
return this.runRoute(continueRoute);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Takes over a req.url a middleware assigned. On an ordinary walk the scan simply continues
|
|
236
|
+
* against the new path; a compiled chain was computed for the old one, so ordinary routing
|
|
237
|
+
* takes over, skipping only what has already run.
|
|
238
|
+
*
|
|
239
|
+
* @param {number} startIndex where dispatch was about to resume
|
|
240
|
+
* @returns {boolean} whether this rerouted the walk itself
|
|
241
|
+
*/
|
|
242
|
+
takeUrlRewrite(startIndex) {
|
|
243
|
+
const req = this.req;
|
|
244
|
+
const router = this.router;
|
|
245
|
+
req._absorbUrlRewrite();
|
|
246
|
+
if (!this.skipCheck) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
|
|
250
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
251
|
+
req._stack.length = 0;
|
|
252
|
+
req._consumed = 0;
|
|
253
|
+
setMountedPath(req);
|
|
254
|
+
}
|
|
255
|
+
this.routes = router._routes;
|
|
256
|
+
this.skipCheck = false;
|
|
257
|
+
this.dispatch(0);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Takes over a req.method a middleware assigned, which method-override does. The ordinary scan
|
|
263
|
+
* reads req.method per route and is right from the next hop. A compiled chain was chosen by the
|
|
264
|
+
* method uWS dispatched on, so ordinary routing takes over from the top.
|
|
265
|
+
*
|
|
266
|
+
* @param {number} startIndex where dispatch was about to resume
|
|
267
|
+
* @returns {boolean} whether this rerouted the walk itself
|
|
268
|
+
*/
|
|
269
|
+
takeMethodRewrite(startIndex) {
|
|
270
|
+
const req = this.req;
|
|
271
|
+
const router = this.router;
|
|
272
|
+
req._absorbMethodRewrite();
|
|
273
|
+
if (!this.skipCheck) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
this.skipUntil = startIndex > 0 ? this.routes[startIndex - 1] : undefined;
|
|
277
|
+
if (req._stack !== null && req._stack.length > 0) {
|
|
278
|
+
req._stack.length = 0;
|
|
279
|
+
req._consumed = 0;
|
|
280
|
+
setMountedPath(req);
|
|
281
|
+
}
|
|
282
|
+
this.routes = router._routes;
|
|
283
|
+
this.skipCheck = false;
|
|
284
|
+
this.dispatch(0);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
|
|
290
|
+
* way in, and then the route's callbacks run one after another through next().
|
|
291
|
+
*
|
|
292
|
+
* @param {any} continueRoute what _preprocessRequest decided: true to run, "route" to skip
|
|
293
|
+
*/
|
|
294
|
+
runRoute(continueRoute) {
|
|
295
|
+
const req = this.req;
|
|
296
|
+
const route = this.route;
|
|
297
|
+
// A compiled chain walks into a mount rather than entering it, so the rule above needs
|
|
298
|
+
// saying here too: everything after this marker is inside the mount, and a mount is stepped
|
|
299
|
+
// over while an error is in flight. Ordinary routing takes over after the mount.
|
|
300
|
+
if (route.keepMount === true && req._error) {
|
|
301
|
+
return this.dispatch(this.routes.length);
|
|
302
|
+
}
|
|
303
|
+
if (route.use) {
|
|
304
|
+
if (route.mountApp) {
|
|
305
|
+
// optimized chain: normal dispatch swaps req.app when it enters a mounted
|
|
306
|
+
// Application, but the compiled mount route has no callback to do it
|
|
307
|
+
rememberApp(this, route, req);
|
|
308
|
+
useApp(req, route.mountApp);
|
|
309
|
+
}
|
|
310
|
+
const taken = mountPrefixLength(route, req);
|
|
311
|
+
// pushed negative when this mount consumes the whole remaining path: express invents
|
|
312
|
+
// the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
|
|
313
|
+
(req._stack ??= []).push(
|
|
314
|
+
taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
|
|
315
|
+
);
|
|
316
|
+
// a use with no path consumes nothing, so everything below would work out the values
|
|
317
|
+
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
318
|
+
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
319
|
+
if (taken !== 0 || req.endsWithSlash) {
|
|
320
|
+
req._consumed += taken;
|
|
321
|
+
// a mount that took a trailing slash: req.baseUrl then has to join the pieces
|
|
322
|
+
// rather than slice the path, which is the slower half of its getter
|
|
323
|
+
if (taken !== 0 && req._originalPath.charCodeAt(req._consumed - 1) === 0x2f) {
|
|
324
|
+
req._mountSlash = true;
|
|
325
|
+
}
|
|
326
|
+
setMountedPath(req);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
req.next = this.next;
|
|
330
|
+
// the same step when the route has one callback, and then it has to be the same object:
|
|
331
|
+
// express hands res.format's handlers the next its own layer received, and its test asserts
|
|
332
|
+
// that identity. With more than one callback express hands over the one that leaves the route
|
|
333
|
+
req._leaveRoute = route.callbacks.length > 1 ? (this.leaveRoute ??= this.stepOutOfRoute.bind(this)) : this.next;
|
|
334
|
+
if (continueRoute === "route") {
|
|
335
|
+
this.step("route");
|
|
336
|
+
} else if (continueRoute) {
|
|
337
|
+
this.step(undefined);
|
|
338
|
+
} else {
|
|
339
|
+
this.resolve(true);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A hop while the request carries an error, or over an error handler it cannot run: the
|
|
345
|
+
* handler is invoked when the error is its to catch, everything else is skipped.
|
|
346
|
+
*
|
|
347
|
+
* @param {number} kind what the callback is, one of the CALLBACK_ constants
|
|
348
|
+
* @param {Function} callback
|
|
349
|
+
*/
|
|
350
|
+
errorHop(kind, callback) {
|
|
351
|
+
const req = this.req;
|
|
352
|
+
const route = this.route;
|
|
353
|
+
// A four argument handler written inside a route only sees what that route raised: express
|
|
354
|
+
// skips a route layer while an error is in flight, so an error from a middleware before it,
|
|
355
|
+
// or out of a mount, walks past to the router's own error handlers. Middleware error
|
|
356
|
+
// handlers keep the ordinary rule.
|
|
357
|
+
const reachable = route.use
|
|
358
|
+
? req._errorKey !== undefined && route.routeKey >= req._errorKey
|
|
359
|
+
: route.routeKey === req._errorKey || (route.group !== undefined && route.group === req._errorGroup);
|
|
360
|
+
if (req._error && kind === CALLBACK_ERROR && reachable) {
|
|
361
|
+
const out = this.router._handleError(req._error, callback, req, this.res);
|
|
362
|
+
if (out instanceof Promise) {
|
|
363
|
+
// an error handler's rejected promise moves on to the next error handler, and
|
|
364
|
+
// a bare rejection gets the error express invents for it
|
|
365
|
+
out.catch((err) => {
|
|
366
|
+
req._error = err || new Error("Rejected promise");
|
|
367
|
+
req._errorKey = route.routeKey;
|
|
368
|
+
req._errorGroup = route.group;
|
|
369
|
+
return this.step(undefined);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
return this.step(undefined);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
|
|
379
|
+
* route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
|
|
380
|
+
* here without re-running step's prologue and compares.
|
|
381
|
+
*
|
|
382
|
+
* @param {boolean} isRouter next("router") rather than next("route")
|
|
383
|
+
*/
|
|
384
|
+
leaveHop(isRouter) {
|
|
385
|
+
const req = this.req;
|
|
386
|
+
const route = this.route;
|
|
387
|
+
if (route.use && !route.keepMount) {
|
|
388
|
+
const pushed = req._stack.pop();
|
|
389
|
+
const taken = pushed < 0 ? -pushed : pushed;
|
|
390
|
+
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
391
|
+
// req.url from the original path and would silently revert it. The slashAdded
|
|
392
|
+
// mangle belongs to the mount that consumed a prefix, not to a pathless use
|
|
393
|
+
if (req.url !== req._lastUrl) {
|
|
394
|
+
req._absorbUrlRewrite(taken !== 0);
|
|
395
|
+
req._consumed -= taken;
|
|
396
|
+
setMountedPath(req);
|
|
397
|
+
} else {
|
|
398
|
+
if (pushed < 0 && req._originalPath.length > req._consumed) {
|
|
399
|
+
// a rewrite below this mount left a remainder where entry had none: express
|
|
400
|
+
// strips the first character of it when it rejoins, see issue #17
|
|
401
|
+
req._originalPath =
|
|
402
|
+
req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
|
|
403
|
+
req._mayFailDecode = null;
|
|
404
|
+
}
|
|
405
|
+
if (taken !== 0) {
|
|
406
|
+
// a pathless use consumed nothing and rewrote nothing, so the recompute would
|
|
407
|
+
// write back the very values it reads
|
|
408
|
+
req._consumed -= taken;
|
|
409
|
+
setMountedPath(req);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
restoreApp(route, req);
|
|
413
|
+
}
|
|
414
|
+
if (isRouter) {
|
|
415
|
+
if (this.skipCheck) {
|
|
416
|
+
// on a compiled chain, leaving the router is what running out of chain already
|
|
417
|
+
// means: ordinary routing takes over after the mount. With no mount in the chain
|
|
418
|
+
// the router being left is the app's own, and nothing of it may run afterwards
|
|
419
|
+
if (this.skipUntil?.keepMount) {
|
|
420
|
+
return this.dispatch(this.routes.length);
|
|
421
|
+
}
|
|
422
|
+
return this.resolve(false);
|
|
423
|
+
}
|
|
424
|
+
// out of this router entirely, so whoever mounted it carries on after the
|
|
425
|
+
// mount. The app's own walk has nobody after it, and answers 404
|
|
426
|
+
return this.resolve(false);
|
|
427
|
+
}
|
|
428
|
+
req.routeCount++;
|
|
429
|
+
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
430
|
+
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
431
|
+
try {
|
|
432
|
+
return this.dispatch(this.routeIndex + 1);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
return this.reject(err);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* One hop, which is what next() does: with nothing, run the route's next callback; with "route",
|
|
440
|
+
* leave the route; with anything else, remember it as the error and carry on.
|
|
441
|
+
*
|
|
442
|
+
* @param {any} thingamabob what next() was called with: nothing, "route", or an error
|
|
443
|
+
*/
|
|
444
|
+
step(thingamabob) {
|
|
445
|
+
const req = this.req;
|
|
446
|
+
const res = this.res;
|
|
447
|
+
const route = this.route;
|
|
448
|
+
const router = this.router;
|
|
449
|
+
if (thingamabob) {
|
|
450
|
+
if (thingamabob === "route" || thingamabob === "router") {
|
|
451
|
+
return this.leaveHop(thingamabob === "router");
|
|
452
|
+
} else {
|
|
453
|
+
req._error = thingamabob;
|
|
454
|
+
req._errorKey = route.routeKey;
|
|
455
|
+
req._errorGroup = route.group;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const kind = route.callbackKinds[this.callbackIndex];
|
|
459
|
+
const callback = route.callbacks[this.callbackIndex++];
|
|
460
|
+
if (!callback) {
|
|
461
|
+
return this.leaveHop(false);
|
|
462
|
+
}
|
|
463
|
+
// skipping routes we already went through via optimized path. Before the Router branch
|
|
464
|
+
// below and not after it: a mount whose chain was compiled has already run, and running it
|
|
465
|
+
// again would answer from inside the router a request that had just left it
|
|
466
|
+
if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
|
|
467
|
+
return this.step(undefined);
|
|
468
|
+
}
|
|
469
|
+
// A mounted router or application is stepped over while an error is in flight. Its handle
|
|
470
|
+
// takes three arguments, so express's Layer#handleError hands the error straight on: what a
|
|
471
|
+
// mount catches is what it raised itself. Entering it left req.app pointing at the mounted
|
|
472
|
+
// application, whose settings answered, and a 500 carried an ETag under etag false.
|
|
473
|
+
if (kind === CALLBACK_ROUTER && !req._error) {
|
|
474
|
+
if (callback._isApplication) {
|
|
475
|
+
rememberApp(this, route, req);
|
|
476
|
+
useApp(req, callback);
|
|
477
|
+
}
|
|
478
|
+
const pushedParams = callback._settings.mergeParams;
|
|
479
|
+
if (pushedParams) {
|
|
480
|
+
(req._paramStack ??= []).push(req.params);
|
|
481
|
+
}
|
|
482
|
+
// express restores req.params when a router hands back, so what runs after the mount
|
|
483
|
+
// sees the params it had before it
|
|
484
|
+
const parentParams = req.params;
|
|
485
|
+
// each router answers OPTIONS with the verbs it knows itself, so the one being entered
|
|
486
|
+
// starts its own list: express keeps that list per router, and a router that hands back
|
|
487
|
+
// without answering leaves the outer one's untouched
|
|
488
|
+
const parentMethods = req._matchedMethods;
|
|
489
|
+
if (parentMethods !== null) {
|
|
490
|
+
req._matchedMethods = new Set();
|
|
491
|
+
}
|
|
492
|
+
callback
|
|
493
|
+
._routeRequest(req, res, 0)
|
|
494
|
+
.then((routed) => {
|
|
495
|
+
// the child's params are scoped to it, and must not leak into the routes after
|
|
496
|
+
if (pushedParams) {
|
|
497
|
+
req._paramStack.pop();
|
|
498
|
+
}
|
|
499
|
+
req.params = parentParams;
|
|
500
|
+
if (req._error) {
|
|
501
|
+
req._errorKey = route.routeKey;
|
|
502
|
+
req._errorGroup = route.group;
|
|
503
|
+
}
|
|
504
|
+
if (routed) {
|
|
505
|
+
if (parentMethods !== null) {
|
|
506
|
+
req._matchedMethods = parentMethods;
|
|
507
|
+
}
|
|
508
|
+
return this.resolve(true);
|
|
509
|
+
}
|
|
510
|
+
const childMethods = req._matchedMethods;
|
|
511
|
+
if (parentMethods !== null) {
|
|
512
|
+
req._matchedMethods = parentMethods;
|
|
513
|
+
}
|
|
514
|
+
if (req._isOptions && childMethods !== null && childMethods.size && !req._error) {
|
|
515
|
+
// OPTIONS routing is different, it stops in the router if matched.
|
|
516
|
+
// Express answers as the router hands back, so a throw while answering,
|
|
517
|
+
// a head already written being the way, walks on to later error handlers
|
|
518
|
+
try {
|
|
519
|
+
router._sendOptionsReply(req, res, childMethods);
|
|
520
|
+
return this.resolve(true);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return this.step(err);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// An error carried out of the mount is not answered by the automatic reply, and
|
|
526
|
+
// stopping here handed it to the default page: express walks on to the error
|
|
527
|
+
// handlers written after the mount, for OPTIONS as for any other method.
|
|
528
|
+
this.step(undefined);
|
|
529
|
+
})
|
|
530
|
+
// a rejection out of the nested walk, or a throw above, must reject this one
|
|
531
|
+
// instead of dying as an unhandled rejection; wrapped for the native pair's
|
|
532
|
+
// receiver
|
|
533
|
+
.catch((err) => this.reject(err));
|
|
534
|
+
} else {
|
|
535
|
+
// errors and error handlers live out of line: this is the cold path, and its size
|
|
536
|
+
// was pushing step past the inlining threshold
|
|
537
|
+
if (req._error || kind === CALLBACK_ERROR) {
|
|
538
|
+
return this.errorHop(kind, callback);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
// handling OPTIONS method
|
|
543
|
+
if (req._isOptions && !route.all && route.method !== "OPTIONS") {
|
|
544
|
+
// an OPTIONS request always carries the set, see the Request constructor
|
|
545
|
+
const matched = /** @type {Set<string>} */ (req._matchedMethods);
|
|
546
|
+
matched.add(route.method);
|
|
547
|
+
if (route.gettable) {
|
|
548
|
+
matched.add("HEAD");
|
|
549
|
+
}
|
|
550
|
+
return this.step(undefined);
|
|
551
|
+
}
|
|
552
|
+
// entered only so its param callbacks could run, see the scan in dispatch: the verb
|
|
553
|
+
// cannot serve a HEAD, so nothing here answers it
|
|
554
|
+
if (req._isHead && !route.all && !route.gettable && route.method !== "HEAD") {
|
|
555
|
+
return this.step(undefined);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const out = callback(req, res, this.next);
|
|
559
|
+
if (out instanceof Promise) {
|
|
560
|
+
// Express 5 forwards a rejected handler promise to the error middleware on its
|
|
561
|
+
// own, so there is nothing for "catch async errors" or express-async-errors to
|
|
562
|
+
// opt into. A bare rejection carries no error, and express invents this one
|
|
563
|
+
out.catch((err) => {
|
|
564
|
+
req._error = err || new Error("Rejected promise");
|
|
565
|
+
req._errorKey = route.routeKey;
|
|
566
|
+
req._errorGroup = route.group;
|
|
567
|
+
return this.step(undefined);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
req._error = err;
|
|
572
|
+
req._errorKey = route.routeKey;
|
|
573
|
+
req._errorGroup = route.group;
|
|
574
|
+
return this.step(undefined);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
module.exports = Walk;
|
package/src/websocket.js
CHANGED
|
@@ -18,10 +18,13 @@ limitations under the License.
|
|
|
18
18
|
|
|
19
19
|
const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.js");
|
|
20
20
|
|
|
21
|
+
/** @typedef {import("./router.js")} Router */
|
|
22
|
+
/** @typedef {import("./application.js").Application} Application */
|
|
23
|
+
|
|
21
24
|
// the parameter names in a path, in the order µWS numbers them
|
|
22
25
|
const PARAM = /:(\w+)/g;
|
|
23
26
|
|
|
24
|
-
// Handlers
|
|
27
|
+
// Handlers uWS calls with the socket. Everything else in a behavior object is a uWS setting
|
|
25
28
|
// (maxPayloadLength, idleTimeout, compression, ...) and rides through untouched.
|
|
26
29
|
const SOCKET_HANDLERS = ["open", "message", "dropped", "drain", "close", "ping", "pong", "subscription"];
|
|
27
30
|
|
|
@@ -46,11 +49,10 @@ function joinPaths(prefix, path) {
|
|
|
46
49
|
/**
|
|
47
50
|
* Every websocket route reachable from this router, with the mount paths already applied.
|
|
48
51
|
*
|
|
49
|
-
* Walked separately from the HTTP routes: those fall back to ordinary routing when
|
|
50
|
-
* match them,
|
|
51
|
-
* refused out loud instead.
|
|
52
|
+
* Walked separately from the HTTP routes: those fall back to ordinary routing when uWS cannot
|
|
53
|
+
* match them, a websocket has no fallback, so an unmountable one is refused out loud.
|
|
52
54
|
*
|
|
53
|
-
* @param {
|
|
55
|
+
* @param {Router} router
|
|
54
56
|
* @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
|
|
55
57
|
* shape µWS cannot match, which makes everything below it unreachable
|
|
56
58
|
* @param {any[]} out
|
|
@@ -97,11 +99,10 @@ function collectRoutes(router, prefix, out, seen) {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
|
-
* The
|
|
101
|
-
*
|
|
102
|
-
* answered the request itself.
|
|
102
|
+
* The uWS upgrade handler for one route: builds this project's request and response, offers them
|
|
103
|
+
* to the application's own `upgrade` hook, and completes the handshake unless that hook answered.
|
|
103
104
|
*
|
|
104
|
-
* @param {
|
|
105
|
+
* @param {Application} app the application whose request and response classes serve this route
|
|
105
106
|
* @param {string} path the composed path, whose parameters are read back by index
|
|
106
107
|
* @param {any} behavior what the caller registered
|
|
107
108
|
* @returns {(res: any, req: any, context: any) => void}
|
|
@@ -111,7 +112,7 @@ function makeUpgradeHandler(app, path, behavior) {
|
|
|
111
112
|
const userUpgrade = behavior.upgrade;
|
|
112
113
|
|
|
113
114
|
return (res, req, context) => {
|
|
114
|
-
// read off the
|
|
115
|
+
// read off the uWS request before anything can await: it is neutered on return, and the
|
|
115
116
|
// handshake needs these three even when the upgrade is decided asynchronously
|
|
116
117
|
const key = req.getHeader("sec-websocket-key");
|
|
117
118
|
const protocol = req.getHeader("sec-websocket-protocol");
|
|
@@ -169,17 +170,16 @@ function makeUpgradeHandler(app, path, behavior) {
|
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
// an async hook
|
|
173
|
-
//
|
|
174
|
-
// handler, which is the only place µWS accepts it
|
|
173
|
+
// an async hook outlives this callback, so uWS has to be told who to call if the client
|
|
174
|
+
// leaves first. Registered now, inside the handler, the only place uWS accepts it
|
|
175
175
|
res.onAborted(() => {
|
|
176
176
|
aborted = true;
|
|
177
177
|
// and on the response too, so a hook that is still awaiting can see the client left
|
|
178
178
|
// rather than working on towards a handshake nobody is waiting for
|
|
179
179
|
response.aborted = true;
|
|
180
180
|
});
|
|
181
|
-
//
|
|
182
|
-
//
|
|
181
|
+
// whatever the hook writes now lands outside the cork uWS holds for this callback, so the
|
|
182
|
+
// response opens its own, exactly as a route handler answering late does
|
|
183
183
|
response._corkNeeded = true;
|
|
184
184
|
decision.then(accept, (err) => {
|
|
185
185
|
if (!aborted && !response.finished) {
|
|
@@ -193,11 +193,10 @@ function makeUpgradeHandler(app, path, behavior) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Hands every websocket route
|
|
197
|
-
*
|
|
198
|
-
* covers the same path, so the two live side by side.
|
|
196
|
+
* Hands every websocket route to uWS. Called from listen(), before the catch-all: uWS routes an
|
|
197
|
+
* upgrade to the websocket route even when a catch-all covers the same path.
|
|
199
198
|
*
|
|
200
|
-
* @param {
|
|
199
|
+
* @param {Application} app
|
|
201
200
|
*/
|
|
202
201
|
function registerWebSocketRoutes(app) {
|
|
203
202
|
const routes = [];
|
|
@@ -217,7 +216,7 @@ function registerWebSocketRoutes(app) {
|
|
|
217
216
|
* used: a handler under a misspelled name would otherwise never run and never say why.
|
|
218
217
|
*
|
|
219
218
|
* @param {string} path
|
|
220
|
-
* @param {any} behavior
|
|
219
|
+
* @param {any} behavior uWS's WebSocketBehavior, whose shipped typings do not describe it
|
|
221
220
|
*/
|
|
222
221
|
function checkBehavior(path, behavior) {
|
|
223
222
|
if (typeof path !== "string") {
|