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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.13.3",
3
+ "version": "5.14.0",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -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
- bodyError(err.message, err.status ?? err.statusCode ?? 403, err.type ?? "entity.verify.failed", {
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
- return next(bodyError(strictSyntaxMessage(text, first), 400, "entity.parse.failed", { body: text }));
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
- // the JSON error's own message, which is what body-parser keeps, so an application
1257
- // showing err.message still says where the parse gave up
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(bodyError(err.message, 400, "entity.parse.failed", { body: text }));
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
- upgrade?: (req: e.Request, res: e.Response) => void | Promise<void>;
140
- open?: (ws: FulmineWebSocket) => void;
141
- message?: (ws: FulmineWebSocket, message: ArrayBuffer, isBinary: boolean) => void;
142
- dropped?: (ws: FulmineWebSocket, message: ArrayBuffer, isBinary: boolean) => void;
143
- drain?: (ws: FulmineWebSocket) => void;
144
- close?: (ws: FulmineWebSocket, code: number, message: ArrayBuffer) => void;
145
- ping?: (ws: FulmineWebSocket, message: ArrayBuffer) => void;
146
- pong?: (ws: FulmineWebSocket, message: ArrayBuffer) => void;
147
- subscription?: (ws: FulmineWebSocket, topic: ArrayBuffer, newCount: number, oldCount: number) => void;
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