WebSocket Interceptors
WebSocket Interceptors function identically to HTTP Interceptors. They sit between the incoming event and the event handler, allowing you to wrap the execution stream using RxJS Observables.
Overview
Interceptors are incredibly powerful tools for Aspect-Oriented Programming (AOP). They allow you to bind extra logic before or after a WebSocket event handler executes, mutate the returned data, or measure execution time.
Because NestJS heavily relies on RxJS, Interceptors use the Observable.pipe() method to intercept the response generated by your @SubscribeMessage() handlers.
Key Concepts
CallHandler.handle(): This method invokes your actual@SubscribeMessage()handler. Everything you write before calling this happens on the inbound request. Everything you write inside the.pipe()happens on the outbound response.- Context Switching: Just like Guards, you must use
context.switchToWs()to access the Socket client and event data. - Transforming Responses: Interceptors are the best place to wrap raw return data in a standard API envelope (e.g., turning
{ score: 10 }into{ status: 'success', data: { score: 10 } }).
Code Examples
1. Creating a Logging Interceptor
This interceptor measures how long a WebSocket event handler takes to execute.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Socket } from 'socket.io';
@Injectable()
export class WsLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const wsContext = context.switchToWs();
const client: Socket = wsContext.getClient();
const data = wsContext.getData();
console.log(`[WS-IN] Event from ${client.id} with payload:`, data);
const startTime = Date.now();
// Call the actual handler method
return next.handle().pipe(
tap((response) => {
// This executes AFTER the handler finishes
const duration = Date.now() - startTime;
console.log(`[WS-OUT] Completed in ${duration}ms. Returning:`, response);
}),
);
}
}
2. Response Transformation Interceptor
If your event handlers return simple objects, you might want to standardize the acknowledgment payload sent back to the client.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface StandardResponse<T> {
status: 'success';
timestamp: string;
data: T;
}
@Injectable()
export class TransformResponseInterceptor<T> implements NestInterceptor<T, StandardResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<StandardResponse<T>> {
return next.handle().pipe(
map(data => ({
status: 'success',
timestamp: new Date().toISOString(),
data: data // The original data returned by the handler
})),
);
}
}
3. Applying Interceptors
Apply them using the @UseInterceptors() decorator.
import { WebSocketGateway, SubscribeMessage, UseInterceptors } from '@nestjs/websockets';
@WebSocketGateway()
@UseInterceptors(WsLoggingInterceptor, TransformResponseInterceptor)
export class GameGateway {
@SubscribeMessage('get_score')
handleScore() {
// The handler returns a simple object
return { score: 100 };
// The client will actually receive:
// { status: 'success', timestamp: '...', data: { score: 100 } }
}
}
Best Practices
- Timeout Interceptors: WebSocket requests don’t inherently time out like HTTP requests. You can create a
TimeoutInterceptorusing RxJStimeout(5000). If the handler takes longer than 5 seconds, the interceptor throws aWsException, preventing the client from hanging forever waiting for an acknowledgment. - Handling RxJS Streams: Remember that Interceptors deal with RxJS Observables. If your event handler returns an RxJS
SubjectorObservable(sending multiple messages over time), the interceptor’smapoperator will apply to every single emission in that stream!