mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
feat(websockets): add disconnect reason parameter
This change enhances the WebSocket disconnect handling by providing the disconnect reason as an optional second parameter to the handleDisconnect method. Changes: - Add optional reason parameter to OnGatewayDisconnect interface - Update NestGateway interface to support disconnect reason - Modify WebSocketsController to capture and forward disconnect reason - Enhance IoAdapter to extract reason from Socket.IO disconnect events - Maintain full backward compatibility with existing implementations - Add comprehensive unit and integration tests The disconnect reason helps developers understand why clients disconnect, enabling better error handling and debugging. Common reasons include 'client namespace disconnect', 'transport close', 'ping timeout', etc. This change is fully backward compatible - existing code continues to work without modification while new code can optionally access the disconnect reason. Closes #15437 Signed-off-by: snowykte0426 <snowykte0426@naver.com>
This commit is contained in:
@@ -37,6 +37,12 @@ export class RpcExceptionsHandler extends BaseRpcExceptionFilter {
|
||||
exception: T,
|
||||
host: ArgumentsHost,
|
||||
): Observable<any> | null {
|
||||
const filters = this.filters.filter(
|
||||
filter => filter.exceptionMetatypes?.length === 0,
|
||||
);
|
||||
if (filters.length > 0) {
|
||||
return filters[0].func(exception, host);
|
||||
}
|
||||
if (isEmpty(this.filters)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,10 @@ export class IoAdapter extends AbstractWsAdapter {
|
||||
return { data: payload };
|
||||
}
|
||||
|
||||
public bindClientDisconnect(client: Socket, callback: Function) {
|
||||
client.on(DISCONNECT_EVENT, (reason: string) => callback(reason));
|
||||
}
|
||||
|
||||
public async close(server: Server): Promise<void> {
|
||||
if (this.forceCloseConnections && server.httpServer === this.httpServer) {
|
||||
return;
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
* @publicApi
|
||||
*/
|
||||
export interface OnGatewayDisconnect<T = any> {
|
||||
handleDisconnect(client: T): any;
|
||||
handleDisconnect(client: T, reason?: string): any;
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
export interface NestGateway {
|
||||
afterInit?: (server: any) => void;
|
||||
handleConnection?: (...args: any[]) => void;
|
||||
handleDisconnect?: (client: any) => void;
|
||||
handleDisconnect?: (client: any, reason?: string) => void;
|
||||
}
|
||||
|
||||
@@ -426,6 +426,72 @@ describe('WebSocketsController', () => {
|
||||
instance.subscribeDisconnectEvent(gateway, event);
|
||||
expect(subscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when handling disconnect events', () => {
|
||||
let handleDisconnectSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
handleDisconnectSpy = vi.fn();
|
||||
(gateway as any).handleDisconnect = handleDisconnectSpy;
|
||||
});
|
||||
|
||||
it('should call handleDisconnect with client and reason when data contains both', () => {
|
||||
const mockClient = { id: 'test-client' };
|
||||
const mockReason = 'client namespace disconnect';
|
||||
const disconnectData = { client: mockClient, reason: mockReason };
|
||||
|
||||
let subscriptionCallback: Function | undefined;
|
||||
event.subscribe = (callback: Function) => {
|
||||
subscriptionCallback = callback;
|
||||
};
|
||||
|
||||
instance.subscribeDisconnectEvent(gateway, event);
|
||||
|
||||
if (subscriptionCallback) {
|
||||
subscriptionCallback(disconnectData);
|
||||
}
|
||||
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledOnce();
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledWith(
|
||||
mockClient,
|
||||
mockReason,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call handleDisconnect with only client for backward compatibility', () => {
|
||||
const mockClient = { id: 'test-client' };
|
||||
|
||||
let subscriptionCallback: Function | undefined;
|
||||
event.subscribe = (callback: Function) => {
|
||||
subscriptionCallback = callback;
|
||||
};
|
||||
|
||||
instance.subscribeDisconnectEvent(gateway, event);
|
||||
|
||||
if (subscriptionCallback) {
|
||||
subscriptionCallback(mockClient);
|
||||
}
|
||||
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledOnce();
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledWith(mockClient);
|
||||
});
|
||||
|
||||
it('should handle null/undefined data gracefully', () => {
|
||||
let subscriptionCallback: Function | undefined;
|
||||
event.subscribe = (callback: Function) => {
|
||||
subscriptionCallback = callback;
|
||||
};
|
||||
|
||||
instance.subscribeDisconnectEvent(gateway, event);
|
||||
|
||||
if (subscriptionCallback) {
|
||||
subscriptionCallback(null);
|
||||
}
|
||||
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledOnce();
|
||||
expect(handleDisconnectSpy).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('subscribeMessages', () => {
|
||||
const gateway = new Test();
|
||||
|
||||
@@ -142,7 +142,9 @@ export class WebSocketsController {
|
||||
|
||||
const disconnectHook = adapter.bindClientDisconnect;
|
||||
disconnectHook &&
|
||||
disconnectHook.call(adapter, client, () => disconnect.next(client));
|
||||
disconnectHook.call(adapter, client, (reason?: string) =>
|
||||
disconnect.next({ client, reason }),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -164,9 +166,15 @@ export class WebSocketsController {
|
||||
|
||||
public subscribeDisconnectEvent(instance: NestGateway, event: Subject<any>) {
|
||||
if (instance.handleDisconnect) {
|
||||
event
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe(instance.handleDisconnect.bind(instance));
|
||||
event.pipe(distinctUntilChanged()).subscribe((data: any) => {
|
||||
// Handle both old format (just client) and new format ({ client, reason })
|
||||
if (data && typeof data === 'object' && 'client' in data) {
|
||||
instance.handleDisconnect!(data.client, data.reason);
|
||||
} else {
|
||||
// Backward compatibility: if it's just the client
|
||||
instance.handleDisconnect!(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user