Skip to content

Commit f760dae

Browse files
veeceeyclaude
andcommitted
fix(websockets): detect native ws clients before calling emit
The emitMessage() method checked for client.emit first, but native WebSocket clients from the ws package inherit from EventEmitter and also have an emit method that only dispatches events locally. This caused WsException errors to be emitted as local events instead of being sent over the wire, resulting in the e2e test timing out. Added an isNativeWebSocket() check that looks for a numeric readyState property (per the WebSocket spec) to reliably distinguish native WS clients from Socket.IO sockets. Also fixed prettier formatting on the emitMessage generic signature, and updated test mocks to include readyState on the native WS client stub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent feb10ca commit f760dae

File tree

2 files changed

+32
-9
lines changed

2 files changed

+32
-9
lines changed

packages/websockets/exceptions/base-ws-exception-filter.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import {
44
Logger,
55
WsExceptionFilter,
66
} from '@nestjs/common';
7-
import { isFunction, isObject } from '@nestjs/common/utils/shared.utils';
7+
import {
8+
isFunction,
9+
isNumber,
10+
isObject,
11+
} from '@nestjs/common/utils/shared.utils';
812
import { MESSAGES } from '@nestjs/core/constants';
913
import { WsException } from '../errors/ws-exception';
1014

@@ -120,19 +124,38 @@ export class BaseWsExceptionFilter<
120124
/**
121125
* Sends an error message to the client. Supports both Socket.IO clients
122126
* (which use `emit`) and native WebSocket clients (which use `send`).
127+
*
128+
* Native WebSocket clients (e.g. from the `ws` package) inherit from
129+
* EventEmitter and therefore also have an `emit` method, but that method
130+
* only dispatches events locally. To distinguish native WebSocket clients
131+
* from Socket.IO clients, we check for a numeric `readyState` property
132+
* (part of the WebSocket specification) before falling back to `emit`.
123133
*/
124-
protected emitMessage<
125-
TClient extends { emit?: Function; send?: Function },
126-
>(client: TClient, event: string, payload: unknown): void {
127-
if (isFunction(client.emit)) {
128-
client.emit(event, payload);
129-
} else if (isFunction(client.send)) {
134+
protected emitMessage<TClient extends { emit?: Function; send?: Function }>(
135+
client: TClient,
136+
event: string,
137+
payload: unknown,
138+
): void {
139+
if (this.isNativeWebSocket(client)) {
130140
client.send(
131141
JSON.stringify({
132142
event,
133143
data: payload,
134144
}),
135145
);
146+
} else if (isFunction(client.emit)) {
147+
client.emit(event, payload);
136148
}
137149
}
150+
151+
/**
152+
* Determines whether the given client is a native WebSocket (e.g. from the
153+
* `ws` package) as opposed to a Socket.IO socket. Native WebSocket objects
154+
* expose a numeric `readyState` property per the WebSocket specification.
155+
*/
156+
private isNativeWebSocket(
157+
client: Record<string, any>,
158+
): client is { send: Function; readyState: number } {
159+
return isNumber(client.readyState) && isFunction(client.send);
160+
}
138161
}

packages/websockets/test/exceptions/ws-exceptions-handler.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ describe('WsExceptionsHandler', () => {
106106

107107
describe('when client uses "send" instead of "emit" (native WebSocket)', () => {
108108
let sendStub: sinon.SinonStub;
109-
let wsClient: { send: sinon.SinonStub };
109+
let wsClient: { send: sinon.SinonStub; readyState: number };
110110
let wsExecutionContextHost: ExecutionContextHost;
111111

112112
beforeEach(() => {
113113
handler = new WsExceptionsHandler();
114114
sendStub = sinon.stub();
115-
wsClient = { send: sendStub };
115+
wsClient = { send: sendStub, readyState: 1 };
116116
wsExecutionContextHost = new ExecutionContextHost([
117117
wsClient,
118118
data,

0 commit comments

Comments
 (0)