@nestjs/platform-ws 12.0.0 → 12.0.2

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.
@@ -19,6 +19,7 @@ export declare class WsAdapter extends AbstractWsAdapter {
19
19
  protected readonly logger: Logger;
20
20
  protected readonly httpServersRegistry: Map<number, any>;
21
21
  protected readonly wsServersRegistry: Map<number, WsServerRegistryEntry>;
22
+ private readonly upgradeListenersRegistry;
22
23
  protected messageParser: WsMessageParser;
23
24
  constructor(appOrHttpServer?: INestApplicationContext | object, options?: WsAdapterOptions);
24
25
  create(port: number, options?: Record<string, any> & {
@@ -3,7 +3,7 @@ import { AbstractWsAdapter } from '@nestjs/websockets';
3
3
  import * as http from 'http';
4
4
  import { createRequire } from 'module';
5
5
  import { EMPTY, fromEvent } from 'rxjs';
6
- import { filter, first, mergeMap, share, takeUntil } from 'rxjs/operators';
6
+ import { catchError, filter, first, mergeMap, share, takeUntil, } from 'rxjs/operators';
7
7
  import { loadPackageSync, isNil, normalizePath } from '@nestjs/common/internal';
8
8
  import { CLOSE_EVENT, CONNECTION_EVENT, ERROR_EVENT, } from '@nestjs/websockets/internal';
9
9
  let wsPackage = {};
@@ -15,6 +15,11 @@ var READY_STATE;
15
15
  READY_STATE[READY_STATE["CLOSED_STATE"] = 3] = "CLOSED_STATE";
16
16
  })(READY_STATE || (READY_STATE = {}));
17
17
  const UNDERLYING_HTTP_SERVER_PORT = 0;
18
+ // Kept out of the class so that an adapter can be compared against it to tell
19
+ // whether the parser in use is still the built-in one.
20
+ const defaultMessageParser = (data) => {
21
+ return JSON.parse(data.toString());
22
+ };
18
23
  /**
19
24
  * @publicApi
20
25
  */
@@ -22,9 +27,8 @@ export class WsAdapter extends AbstractWsAdapter {
22
27
  logger = new Logger(WsAdapter.name);
23
28
  httpServersRegistry = new Map();
24
29
  wsServersRegistry = new Map();
25
- messageParser = (data) => {
26
- return JSON.parse(data.toString());
27
- };
30
+ upgradeListenersRegistry = new Map();
31
+ messageParser = defaultMessageParser;
28
32
  constructor(appOrHttpServer, options) {
29
33
  super(appOrHttpServer);
30
34
  wsPackage = loadPackageSync('ws', 'WsAdapter', () => createRequire(import.meta.url)('ws'));
@@ -80,7 +84,14 @@ export class WsAdapter extends AbstractWsAdapter {
80
84
  const handlersMap = new Map();
81
85
  handlers.forEach(handler => handlersMap.set(handler.message, handler));
82
86
  const close$ = fromEvent(client, CLOSE_EVENT).pipe(share(), first());
83
- const source$ = fromEvent(client, 'message').pipe(mergeMap(data => this.bindMessageHandler(data, handlersMap, transform).pipe(filter(result => !isNil(result)))), takeUntil(close$));
87
+ const source$ = fromEvent(client, 'message').pipe(mergeMap(data => this.bindMessageHandler(data, handlersMap, transform).pipe(filter(result => !isNil(result)),
88
+ // a handler that rejects through an error filter that rethrows
89
+ // would error the client's stream and silence every later message;
90
+ // that rethrow is an app bug, so it gets logged, not dropped silently
91
+ catchError(err => {
92
+ this.logger.error(err);
93
+ return EMPTY;
94
+ }))), takeUntil(close$));
84
95
  const onMessage = (response) => {
85
96
  if (client.readyState !== READY_STATE.OPEN_STATE) {
86
97
  return;
@@ -90,16 +101,37 @@ export class WsAdapter extends AbstractWsAdapter {
90
101
  source$.subscribe(onMessage);
91
102
  }
92
103
  bindMessageHandler(buffer, handlersMap, transform) {
104
+ let message;
93
105
  try {
94
- const message = this.messageParser(buffer.data);
95
- if (!message) {
96
- return EMPTY;
106
+ message = this.messageParser(buffer.data);
107
+ }
108
+ catch (err) {
109
+ // A custom parser that throws is an application bug, and swallowing it
110
+ // leaves no trace of it anywhere. The default parser, on the other hand,
111
+ // throws on client-controlled input, so reporting that one would turn a
112
+ // public socket into a log flood target. A custom parser that wants a
113
+ // frame dropped silently can return nothing, which is handled below.
114
+ if (this.messageParser !== defaultMessageParser) {
115
+ this.logger.error(err);
97
116
  }
98
- const messageHandler = handlersMap.get(message.event);
99
- const { callback } = messageHandler;
100
- return transform(callback(message.data, message.event));
117
+ return EMPTY;
118
+ }
119
+ if (!message) {
120
+ return EMPTY;
121
+ }
122
+ const messageHandler = handlersMap.get(message.event);
123
+ if (!messageHandler) {
124
+ // An unrecognised event is an expected condition on a public socket,
125
+ // not an error. It used to reach the catch below as a TypeError.
126
+ return EMPTY;
127
+ }
128
+ try {
129
+ return transform(messageHandler.callback(message.data, message.event));
101
130
  }
102
131
  catch {
132
+ // Kept deliberately: a synchronous throw here would otherwise propagate
133
+ // through the mergeMap in bindMessageHandlers and tear down the client's
134
+ // source stream, silencing every subsequent message from that client.
103
135
  return EMPTY;
104
136
  }
105
137
  }
@@ -128,9 +160,14 @@ export class WsAdapter extends AbstractWsAdapter {
128
160
  const closeEventSignals = Array.from(this.httpServersRegistry)
129
161
  .filter(([port]) => port !== UNDERLYING_HTTP_SERVER_PORT)
130
162
  .map(([_, server]) => new Promise(resolve => server.close(resolve)));
163
+ // A server passed in by the caller outlives this adapter, so the listener
164
+ // added to it has to be taken off explicitly. Servers created here are
165
+ // closed above and take their listeners with them.
166
+ this.upgradeListenersRegistry.forEach((listener, port) => this.httpServersRegistry.get(port)?.off('upgrade', listener));
131
167
  await Promise.all(closeEventSignals);
132
168
  this.httpServersRegistry.clear();
133
169
  this.wsServersRegistry.clear();
170
+ this.upgradeListenersRegistry.clear();
134
171
  }
135
172
  setMessageParser(parser) {
136
173
  this.messageParser = parser;
@@ -140,7 +177,7 @@ export class WsAdapter extends AbstractWsAdapter {
140
177
  return;
141
178
  }
142
179
  this.httpServersRegistry.set(port, httpServer);
143
- httpServer.on('upgrade', (request, socket, head) => {
180
+ const upgradeListener = (request, socket, head) => {
144
181
  try {
145
182
  const baseUrl = 'ws://' + request.headers.host + '/';
146
183
  const pathname = new URL(request.url, baseUrl).pathname;
@@ -162,7 +199,9 @@ export class WsAdapter extends AbstractWsAdapter {
162
199
  catch (err) {
163
200
  socket.end(`HTTP/1.1 400\r\n${err.message}`);
164
201
  }
165
- });
202
+ };
203
+ httpServer.on('upgrade', upgradeListener);
204
+ this.upgradeListenersRegistry.set(port, upgradeListener);
166
205
  return httpServer;
167
206
  }
168
207
  addWsServerToRegistry(wsServer, port, path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/platform-ws",
3
- "version": "12.0.0",
3
+ "version": "12.0.2",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@platform-ws)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -29,9 +29,9 @@
29
29
  "ws": "8.21.3"
30
30
  },
31
31
  "peerDependencies": {
32
- "@nestjs/common": "^11.0.0",
33
- "@nestjs/websockets": "^11.0.0",
32
+ "@nestjs/common": "^12.0.0",
33
+ "@nestjs/websockets": "^12.0.0",
34
34
  "rxjs": "^7.1.0"
35
35
  },
36
- "gitHead": "6494a6c27400547ea6cd9b78da07b2e9c2547ca0"
36
+ "gitHead": "ee168a569d30e600d301ed38abb14d2171a6f315"
37
37
  }