fulmine.js 5.13.3 → 5.14.0
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/README.md +1 -0
- package/package.json +1 -1
- package/src/middlewares.js +40 -7
- package/src/types.d.ts +28 -9
- package/src/websocket.js +3 -0
package/README.md
CHANGED
|
@@ -551,6 +551,7 @@ app.ws("/room/:id", {
|
|
|
551
551
|
- **The behavior object is µWS's**, settings included: `maxPayloadLength`, `idleTimeout`, `compression`, `maxBackpressure`, `sendPingsAutomatically` and the rest are passed through untouched, as are the `open`, `message`, `drain`, `close`, `ping`, `pong`, `dropped` and `subscription` handlers. The socket is µWS's too, so `send`, `subscribe`, `publish`, `cork` and `getBufferedAmount` behave exactly as its documentation describes.
|
|
552
552
|
- **`upgrade(req, res)` is this project's addition.** It runs before the handshake with the same `Request` and `Response` your routes get, so a session, a token or a header decides whether the socket opens. Answering the response, with `res.sendStatus(401)` or any other write, declines the upgrade. Returning a promise holds the handshake until it settles, which is what an authentication lookup needs.
|
|
553
553
|
- **`ws.req` is that request**, and it outlives the response: the client's address, headers, query and params are readable from any handler for as long as the socket is open. Hanging your own values on it in `upgrade` is how per-connection state gets to `message`.
|
|
554
|
+
- **A hook that awaits can be left holding a dead request.** The client may go while a token is being checked, and µWS frees the response when it does, so `res.aborted` says whether there is still anybody to answer. Writing to a response that was aborted does nothing rather than throwing.
|
|
554
555
|
- **Routers work.** `router.ws("/lobby", ...)` mounted with `app.use("/chat", router)` serves `/chat/lobby`.
|
|
555
556
|
- **Paths are the ones µWS matches**: literal, or with parameters that are a whole segment such as `/room/:id`. Anything else throws where it is written rather than failing to match later.
|
|
556
557
|
- **Broadcasting from outside a socket**: `app.publish(topic, message)` and `app.numSubscribers(topic)`.
|
package/package.json
CHANGED
package/src/middlewares.js
CHANGED
|
@@ -210,9 +210,8 @@ function runVerify(req, res, next, options, buf) {
|
|
|
210
210
|
} catch (e) {
|
|
211
211
|
const err = /** @type {any} */ (e);
|
|
212
212
|
next(
|
|
213
|
-
|
|
214
|
-
body: buf
|
|
215
|
-
stack: err.stack
|
|
213
|
+
asBodyError(err, err.status ?? err.statusCode ?? 403, err.type ?? "entity.verify.failed", {
|
|
214
|
+
body: buf
|
|
216
215
|
})
|
|
217
216
|
);
|
|
218
217
|
return false;
|
|
@@ -249,6 +248,15 @@ function strictSyntaxMessage(text, char) {
|
|
|
249
248
|
return "strict violation";
|
|
250
249
|
}
|
|
251
250
|
|
|
251
|
+
// The name http-errors gives each status body-parser answers with. An application reading
|
|
252
|
+
// err.name, or a logger printing it, sees "PayloadTooLargeError" from Express and would have
|
|
253
|
+
// seen a bare "Error" here.
|
|
254
|
+
const BODY_ERROR_NAMES = {
|
|
255
|
+
400: "BadRequestError",
|
|
256
|
+
413: "PayloadTooLargeError",
|
|
257
|
+
415: "UnsupportedMediaTypeError"
|
|
258
|
+
};
|
|
259
|
+
|
|
252
260
|
/**
|
|
253
261
|
* The error a body parser hands to next(), shaped as body-parser shapes it: with a status, since
|
|
254
262
|
* `res.status(err.status || 500)` would otherwise answer 500 to a request that was merely too
|
|
@@ -262,6 +270,25 @@ function strictSyntaxMessage(text, char) {
|
|
|
262
270
|
*/
|
|
263
271
|
function bodyError(message, status, type, extra) {
|
|
264
272
|
const err = /** @type {any} */ (new Error(message));
|
|
273
|
+
if (BODY_ERROR_NAMES[status]) {
|
|
274
|
+
err.name = BODY_ERROR_NAMES[status];
|
|
275
|
+
}
|
|
276
|
+
return asBodyError(err, status, type, extra);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The same, for an error somebody else made: the SyntaxError JSON.parse threw, or whatever a
|
|
281
|
+
* verify hook threw. http-errors decorates such an error rather than replacing it, so its name,
|
|
282
|
+
* its stack and any property the thrower put on it are all still there when the application
|
|
283
|
+
* reads it.
|
|
284
|
+
*
|
|
285
|
+
* @param {any} err
|
|
286
|
+
* @param {number} status
|
|
287
|
+
* @param {string} type body-parser's own name for the kind of failure
|
|
288
|
+
* @param {object} [extra] anything else body-parser puts on that particular error
|
|
289
|
+
* @returns {Error}
|
|
290
|
+
*/
|
|
291
|
+
function asBodyError(err, status, type, extra) {
|
|
265
292
|
// 4xx is the client's to see; a 5xx here would be the server's own problem and stays hidden
|
|
266
293
|
err.expose = status < 500;
|
|
267
294
|
err.statusCode = status;
|
|
@@ -1246,17 +1273,23 @@ const json = createBodyParser(
|
|
|
1246
1273
|
// eslint-disable-next-line no-control-regex
|
|
1247
1274
|
const first = text.match(/^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/)?.[1];
|
|
1248
1275
|
if (first !== "{" && first !== "[") {
|
|
1249
|
-
|
|
1276
|
+
// a SyntaxError rather than an Error, since that is what body-parser builds here and
|
|
1277
|
+
// what an application testing `err instanceof SyntaxError` looks for
|
|
1278
|
+
return next(
|
|
1279
|
+
asBodyError(new SyntaxError(strictSyntaxMessage(text, first)), 400, "entity.parse.failed", {
|
|
1280
|
+
body: text
|
|
1281
|
+
})
|
|
1282
|
+
);
|
|
1250
1283
|
}
|
|
1251
1284
|
}
|
|
1252
1285
|
|
|
1253
1286
|
try {
|
|
1254
1287
|
req.body = JSON.parse(text, options.reviver);
|
|
1255
1288
|
} catch (e) {
|
|
1256
|
-
//
|
|
1257
|
-
//
|
|
1289
|
+
// V8's own error, which is what body-parser hands on: its message says where the parse
|
|
1290
|
+
// gave up, and it is still the SyntaxError an application may be testing for
|
|
1258
1291
|
const err = /** @type {any} */ (e);
|
|
1259
|
-
return next(
|
|
1292
|
+
return next(asBodyError(err, 400, "entity.parse.failed", { body: text }));
|
|
1260
1293
|
}
|
|
1261
1294
|
|
|
1262
1295
|
next();
|
package/src/types.d.ts
CHANGED
|
@@ -115,6 +115,13 @@ declare module "fulmine.js" {
|
|
|
115
115
|
export import NextFunction = e.NextFunction;
|
|
116
116
|
export import Locals = e.Locals;
|
|
117
117
|
export import Request = e.Request;
|
|
118
|
+
|
|
119
|
+
// The application, the socket and the behaviour, nameable from outside. `export =` leaves
|
|
120
|
+
// everything declared beside it out of reach of an import, so anything wrapping ws() had
|
|
121
|
+
// to write ReturnType<typeof express> and index into it to say what it takes.
|
|
122
|
+
export type FulmineApplication = Fulmine;
|
|
123
|
+
export type FulmineSocket = FulmineWebSocket;
|
|
124
|
+
export type FulmineWebSocketBehavior = WebSocketBehavior;
|
|
118
125
|
export import RequestHandler = e.RequestHandler;
|
|
119
126
|
export import RequestParamHandler = e.RequestParamHandler;
|
|
120
127
|
export import Response = e.Response;
|
|
@@ -136,15 +143,21 @@ declare module "fulmine.js" {
|
|
|
136
143
|
uWS.WebSocketBehavior<SocketData>,
|
|
137
144
|
"upgrade" | "open" | "message" | "dropped" | "drain" | "close" | "ping" | "pong" | "subscription"
|
|
138
145
|
> & {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
// The return types are µWS's own: it awaits nothing, but open, message and dropped are
|
|
147
|
+
// declared there as returning void or a promise, and narrowing that here made an async
|
|
148
|
+
// handler a type error against the library this wraps.
|
|
149
|
+
// Methods rather than function-typed properties, so a handler may narrow the request or
|
|
150
|
+
// the socket to one carrying what the upgrade hook hung on it, which is how this project
|
|
151
|
+
// says per-connection state is kept
|
|
152
|
+
upgrade?(req: e.Request, res: e.Response): void | Promise<void>;
|
|
153
|
+
open?(ws: FulmineWebSocket): void | Promise<void>;
|
|
154
|
+
message?(ws: FulmineWebSocket, message: ArrayBuffer, isBinary: boolean): void | Promise<void>;
|
|
155
|
+
dropped?(ws: FulmineWebSocket, message: ArrayBuffer, isBinary: boolean): void | Promise<void>;
|
|
156
|
+
drain?(ws: FulmineWebSocket): void;
|
|
157
|
+
close?(ws: FulmineWebSocket, code: number, message: ArrayBuffer): void;
|
|
158
|
+
ping?(ws: FulmineWebSocket, message: ArrayBuffer): void;
|
|
159
|
+
pong?(ws: FulmineWebSocket, message: ArrayBuffer): void;
|
|
160
|
+
subscription?(ws: FulmineWebSocket, topic: ArrayBuffer, newCount: number, oldCount: number): void;
|
|
148
161
|
};
|
|
149
162
|
|
|
150
163
|
// interfaces rather than aliases: `this` is how ws() answers the router or the app it was
|
|
@@ -202,6 +215,12 @@ declare module "fulmine.js" {
|
|
|
202
215
|
// prototype, so a route only has them where that middleware ran, which the optional marks say.
|
|
203
216
|
declare namespace Express {
|
|
204
217
|
interface Response {
|
|
218
|
+
/**
|
|
219
|
+
* Whether the client went away before this response was answered. µWS frees the response
|
|
220
|
+
* then and writing to it does nothing, so a handler that awaited something checks this
|
|
221
|
+
* first. Express has no counterpart, which is why it is optional here.
|
|
222
|
+
*/
|
|
223
|
+
aborted?: boolean;
|
|
205
224
|
/** Adds a mark of your own. A mark with only a description is a legal entry. */
|
|
206
225
|
timing?(name: string, duration?: number, description?: string): this;
|
|
207
226
|
/** Times a piece of work under a name. A promise is timed to where it settles. */
|
package/src/websocket.js
CHANGED
|
@@ -174,6 +174,9 @@ function makeUpgradeHandler(app, path, behavior) {
|
|
|
174
174
|
// handler, which is the only place µWS accepts it
|
|
175
175
|
res.onAborted(() => {
|
|
176
176
|
aborted = true;
|
|
177
|
+
// and on the response too, so a hook that is still awaiting can see the client left
|
|
178
|
+
// rather than working on towards a handshake nobody is waiting for
|
|
179
|
+
response.aborted = true;
|
|
177
180
|
});
|
|
178
181
|
// and whatever the hook writes now lands outside the cork µWS holds for this callback,
|
|
179
182
|
// so the response opens its own, exactly as a route handler answering late does
|