WebSocket Interceptors

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 8 min

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 TimeoutInterceptor using RxJS timeout(5000). If the handler takes longer than 5 seconds, the interceptor throws a WsException, 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 Subject or Observable (sending multiple messages over time), the interceptor’s map operator will apply to every single emission in that stream!