Skip to content

Latest commit

 

History

History
652 lines (513 loc) · 16 KB

File metadata and controls

652 lines (513 loc) · 16 KB

WebSocket Exception Handling

Exception handling in uWestJS using WsException and exception filters.

Table of Contents


Overview

uWestJS provides robust exception handling through:

Note: Exception filters integrate with NestJS and require @nestjs/common for decorators and interfaces.

  • WsException - WebSocket-specific exception class
  • Exception Filters - Catch and handle exceptions
  • Error Responses - Structured error messages to clients

WsException

WebSocket exception that can be caught by exception filters.

import { WsException } from 'uwestjs';

Constructor

constructor(message: string | object, error?: string)

Parameters:

  • message - Error message or error object
  • error - Optional error type/code

Examples:

// Simple message
throw new WsException('Invalid input');

// With error code
throw new WsException('Unauthorized', 'AUTH_ERROR');

// With object message
throw new WsException({
  field: 'email',
  message: 'Invalid email format',
}, 'VALIDATION_ERROR');

Methods

getError()

getError(): { message: string | object; error?: string }

Gets the error response object with consistent structure.

Example:

try {
  throw new WsException('Something went wrong', 'ERROR_CODE');
} catch (exception) {
  const error = exception.getError();
  // { message: 'Something went wrong', error: 'ERROR_CODE' }
}

Using WsException

In Handlers

@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('send-message')
  handleSendMessage(
    @MessageBody() message: string,
    @ConnectedSocket() client: UwsSocket,
  ) {
    if (!client.data?.authenticated) {
      throw new WsException('Not authenticated', 'AUTH_REQUIRED');
    }
    
    if (!message || message.trim().length === 0) {
      throw new WsException('Message cannot be empty', 'INVALID_MESSAGE');
    }
    
    if (message.length > 1000) {
      throw new WsException('Message too long', 'MESSAGE_TOO_LONG');
    }
    
    // Process message
    return { event: 'message-sent', data: { id: '123' } };
  }
}

In Guards

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const client = context.switchToWs().getClient();
    
    if (!client.data?.token) {
      throw new WsException('Token required', 'TOKEN_MISSING');
    }
    
    if (!this.validateToken(client.data.token)) {
      throw new WsException('Invalid token', 'TOKEN_INVALID');
    }
    
    return true;
  }
}

In Pipes

import { Injectable, ArgumentMetadata } from '@nestjs/common';
import { PipeTransform } from '@nestjs/common';
import { WsException } from 'uwestjs';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata): any {
    // metadata provides information about the argument being processed:
    // - type: 'body' | 'query' | 'param' | 'custom'
    // - metatype: The TypeScript type (e.g., String, Number, MessageDto)
    // - data: The parameter name (e.g., 'message', 'userId')
    
    if (!value) {
      throw new WsException(
        `${metadata.data || 'Value'} is required`,
        'VALIDATION_ERROR'
      );
    }
    
    if (typeof value !== 'string') {
      throw new WsException(
        `${metadata.data || 'Value'} must be a string`,
        'TYPE_ERROR'
      );
    }
    
    return value;
  }
}

Custom Exception Filters

Create custom filters to handle exceptions:

import { Catch, ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { WsException } from 'uwestjs';

@Catch(WsException)
export class CustomWsExceptionFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const error = exception.getError();
    
    // Send formatted error to client
    client.emit('error', {
      success: false,
      error: {
        code: error.error || 'UNKNOWN_ERROR',
        message: error.message,
        timestamp: new Date().toISOString(),
      },
    });
  }
}

// Use the filter
@UseFilters(CustomWsExceptionFilter)
@WebSocketGateway()
export class ChatGateway {
  // Handlers
}

Error Response Patterns

Standard Error Response

@Catch(WsException)
export class StandardErrorFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const error = exception.getError();
    
    client.emit('error', {
      status: 'error',
      code: error.error,
      message: error.message,
      timestamp: Date.now(),
    });
  }
}

Detailed Error Response

@Catch(WsException)
export class DetailedErrorFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const data = host.switchToWs().getData();
    const error = exception.getError();
    
    client.emit('error', {
      status: 'error',
      error: {
        code: error.error || 'UNKNOWN',
        message: error.message,
        details: typeof error.message === 'object' ? error.message : undefined,
      },
      request: {
        event: data?.event,
        timestamp: Date.now(),
      },
      client: {
        id: client.id,
      },
    });
  }
}

Logging Error Filter

import { Injectable, Catch, ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { WsException } from 'uwestjs';

// Example custom logger service - replace with your own logging implementation
// You could also use @nestjs/common Logger or a third-party logger like Winston
interface LoggerService {
  error(message: any): void;
}

@Injectable()
@Catch()
export class LoggingErrorFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {} // Inject your custom logger service
  
  catch(exception: unknown, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const data = host.switchToWs().getData();
    
    // Log the error
    this.logger.error({
      message: exception instanceof Error ? exception.message : 'Unknown error',
      clientId: client.id,
      event: data?.event,
      stack: exception instanceof Error ? exception.stack : undefined,
    });
    
    // Send error to client
    const message = exception instanceof WsException
      ? exception.getError().message
      : 'Internal server error';
    
    client.emit('error', {
      message,
      timestamp: Date.now(),
    });
  }
}

Best Practices

1. Use Specific Error Codes

Use specific error codes for different error types:

// Good
throw new WsException('User not found', 'USER_NOT_FOUND');
throw new WsException('Invalid credentials', 'AUTH_FAILED');
throw new WsException('Rate limit exceeded', 'RATE_LIMIT');

// Avoid
throw new WsException('Error');

2. Provide Helpful Error Messages

// Good
throw new WsException('Message length must be between 1 and 1000 characters', 'INVALID_LENGTH');

// Avoid
throw new WsException('Invalid');

3. Use Structured Error Objects

For complex errors, use structured error objects:

throw new WsException({
  field: 'email',
  message: 'Email format is invalid',
  example: 'user@example.com',
}, 'VALIDATION_ERROR');

4. Handle Errors at Appropriate Levels

// Class-level filter for all handlers in a gateway
@UseFilters(GlobalErrorFilter)
@WebSocketGateway()
export class Gateway {
  // Method-level filter for specific handler
  @UseFilters(SpecificErrorFilter)
  @SubscribeMessage('action')
  handleAction() { }
}

5. Register Filters Globally

For application-wide error handling, register filters globally:

Option 1: Register in main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { UwsPlatformAdapter } from 'uwestjs';
import { GlobalWsExceptionFilter } from './filters/global-ws-exception.filter'; // Your custom filter

async function bootstrap() {
  const app = await NestFactory.create(
    AppModule,
    new UwsPlatformAdapter()
  );
  
  // Register global exception filter
  // Note: Global filters registered this way work for HTTP but may not be invoked
  // for WebSocket handlers. Use APP_FILTER provider (Option 2) or class-level
  // @UseFilters() decorator for WebSocket error handling.
  app.useGlobalFilters(new GlobalWsExceptionFilter());
  
  await app.init();
  const adapter = app.get(UwsPlatformAdapter);
  adapter.listen(3000);
}
bootstrap();

Option 2: Use APP_FILTER provider (recommended)

import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalWsExceptionFilter } from './filters/global-ws-exception.filter'; // Your custom filter

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: GlobalWsExceptionFilter,
    },
  ],
})
export class AppModule {}

Option 3: Use class-level @UseFilters() (most reliable for WebSocket)

import { UseFilters } from '@nestjs/common';
import { WebSocketGateway } from '@nestjs/websockets';
import { GlobalWsExceptionFilter } from './filters/global-ws-exception.filter'; // Your custom filter

// Apply filter to all handlers in this gateway
@UseFilters(GlobalWsExceptionFilter)
@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('message')
  handleMessage() { }
  
  @SubscribeMessage('typing')
  handleTyping() { }
}

Benefits of each approach:

  • APP_FILTER provider: Supports dependency injection, instantiated by NestJS DI container
  • Class-level @UseFilters(): Most explicit, guaranteed to work for WebSocket handlers
  • app.useGlobalFilters(): Simple but may not work for WebSocket (works for HTTP)

Example filter with dependency injection:

import { Catch, ArgumentsHost, ExceptionFilter, Injectable } from '@nestjs/common';
import { WsException } from 'uwestjs';

interface LoggerService {
  error(message: any): void;
}

@Injectable()
@Catch()
export class GlobalWsExceptionFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {} // DI works with APP_FILTER and @UseFilters
  
  catch(exception: unknown, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    
    // Log all errors
    this.logger.error({
      clientId: client.id,
      error: exception instanceof Error ? exception.message : 'Unknown',
      stack: exception instanceof Error ? exception.stack : undefined,
    });
    
    // Send standardized error response
    const message = exception instanceof WsException
      ? exception.getError().message
      : 'Internal server error';
    
    client.emit('error', {
      message,
      timestamp: Date.now(),
    });
  }
}

Note: For WebSocket error handling, we recommend using class-level @UseFilters() decorator on your gateways to ensure filters are properly invoked. Global filters registered via app.useGlobalFilters() are primarily designed for HTTP and may not be invoked for WebSocket handlers.

6. Log Errors for Debugging

import { Injectable, Catch, ArgumentsHost, ExceptionFilter } from '@nestjs/common';
import { WsException } from 'uwestjs';

// Example custom logger service - replace with your own logging implementation
// You could also use @nestjs/common Logger or a third-party logger like Winston
interface LoggerService {
  error(message: any): void;
}

@Injectable()
@Catch()
export class LoggingFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {} // Inject your custom logger service
  
  catch(exception: unknown, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    
    // Log for debugging
    this.logger.error({
      clientId: client.id,
      error: exception instanceof Error ? exception.message : 'Unknown',
      stack: exception instanceof Error ? exception.stack : undefined,
    });
    
    // Send user-friendly message to client
    client.emit('error', {
      message: 'An error occurred',
      timestamp: Date.now(),
    });
  }
}

7. Don't Expose Sensitive Information

// Good - Generic error message
client.emit('error', {
  message: 'Authentication failed',
  code: 'AUTH_ERROR',
});

// Avoid - Exposes internal details
client.emit('error', {
  message: 'Database connection failed: Connection refused at 192.168.1.100:5432',
  stack: error.stack,
});

8. Use Different Filters for Different Exception Types

import { Catch, ArgumentsHost, ExceptionFilter, BadRequestException } from '@nestjs/common';
import { WsException } from 'uwestjs';

// Specific filter for WsException
@Catch(WsException)
export class WsExceptionFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    // Handle WsException
  }
}

// Specific filter for validation errors
@Catch(BadRequestException)
export class ValidationFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    // Handle validation errors
  }
}

// Catch-all filter for unexpected errors
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // Handle all other errors
  }
}

Examples

Complete Error Handling Setup

// Custom exception filter
@Catch(WsException)
export class WsExceptionFilter implements ExceptionFilter {
  catch(exception: WsException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const error = exception.getError();
    
    client.emit('error', {
      success: false,
      error: {
        code: error.error || 'UNKNOWN_ERROR',
        message: error.message,
        timestamp: new Date().toISOString(),
      },
    });
  }
}

// Gateway with error handling
@UseFilters(WsExceptionFilter)
@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('send-message')
  handleSendMessage(
    @MessageBody() message: string,
    @ConnectedSocket() client: UwsSocket,
  ) {
    // Validation
    if (!client.data?.authenticated) {
      throw new WsException('Not authenticated', 'AUTH_REQUIRED');
    }
    
    if (!message || message.trim().length === 0) {
      throw new WsException('Message cannot be empty', 'INVALID_MESSAGE');
    }
    
    if (message.length > 1000) {
      throw new WsException('Message too long', 'MESSAGE_TOO_LONG');
    }
    
    // Process message
    return { event: 'message-sent', data: { id: '123' } };
  }
}

Error Codes Enum

export enum WsErrorCode {
  AUTH_REQUIRED = 'AUTH_REQUIRED',
  AUTH_FAILED = 'AUTH_FAILED',
  TOKEN_INVALID = 'TOKEN_INVALID',
  TOKEN_EXPIRED = 'TOKEN_EXPIRED',
  INVALID_MESSAGE = 'INVALID_MESSAGE',
  MESSAGE_TOO_LONG = 'MESSAGE_TOO_LONG',
  RATE_LIMIT = 'RATE_LIMIT',
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  ROOM_NOT_FOUND = 'ROOM_NOT_FOUND',
  PERMISSION_DENIED = 'PERMISSION_DENIED',
}

// Usage
throw new WsException('Not authenticated', WsErrorCode.AUTH_REQUIRED);

Validation Error Handling

import { Catch, ArgumentsHost, ExceptionFilter, BadRequestException } from '@nestjs/common';

@Catch(BadRequestException)
export class ValidationExceptionFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    const client = host.switchToWs().getClient();
    const response = exception.getResponse();
    
    client.emit('validation-error', {
      status: 'error',
      code: 'VALIDATION_ERROR',
      errors: typeof response === 'object' ? response : { message: response },
      timestamp: Date.now(),
    });
  }
}

See Also