NestJS微服务抛403 HttpException网关返回500错误问题排查
问题现象
在NestJS微服务场景中,通过@nestjs/common导入的HttpException抛出403 Forbidden错误时,网关始终将该错误识别为500 Internal Server Error返回。
错误对象序列化结果:
{"response":"User already exists","status":403,"message":"User already exists","name":"HttpException"}
网关侧错误日志:
[Nest] 4747 - 11/06/2022, 18:01:58 ERROR [ExceptionsHandler] Internal server error [Nest] 4747 - 11/06/2022, 18:01:58 ERROR [ExceptionsHandler] undefined
涉及的服务层代码:
import { Model } from 'mongoose'; import { Injectable, Inject, ForbiddenException, HttpException, HttpStatus, } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { User, UserDocument } from './schemas/user.schema'; @Injectable() export class AccountManagementService { constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {} async createUser(user: any): Promise<any> { const existingUser = await this.userModel.find({ $or: [{ username: user.username }, { email: user.email }], }); if (existingUser) { throw new HttpException('User already exists', HttpStatus.FORBIDDEN); } const createdUser = new this.userModel(user); return createdUser.save(); } }
涉及的微服务控制器代码:
import { Controller, Post } from '@nestjs/common'; import { MessagePattern } from '@nestjs/microservices'; import { AccountManagementService } from './account-management.service'; @Controller() export class AccountManagementController { constructor( private accountManagementService: AccountManagementService, ) {} @MessagePattern({ cmd: 'createUser' }) async createUser(userData: any): Promise<any> { return this.accountManagementService .createUser(userData) } }
故障原因
- 核心原因:NestJS的HTTP层和微服务传输层的异常处理逻辑完全独立。
HttpException是HTTP场景专用的异常类,在@MessagePattern修饰的微服务方法中直接抛出时,跨进程传输会丢失异常原型链,网关侧拿到的只是一个普通序列化对象,无法识别为预期的业务异常,就会默认当作未知内部错误,返回500状态码。 - 附带逻辑bug:Mongoose的
find()方法返回匹配结果的数组,即使没有匹配到数据也会返回空数组,空数组在JS布尔判断中为真值,现有代码的if (existingUser)判断会永远成立,无论用户是否存在都会触发抛错。
修复方案
- 微服务端替换异常类:使用微服务专用的
RpcException代替HttpException抛出业务错误,同时修复用户存在性判断逻辑:
// 补充导入RpcException import { RpcException } from '@nestjs/microservices'; // 修正查询和抛错逻辑 async createUser(user: any): Promise<any> { // 改用findOne查询单条记录,性能更好,返回值为null时表示用户不存在 const existingUser = await this.userModel.findOne({ $or: [{ username: user.username }, { email: user.email }], }); if (existingUser) { throw new RpcException({ message: 'User already exists', status: HttpStatus.FORBIDDEN }); } const createdUser = new this.userModel(user); return createdUser.save(); }
- 网关层添加异常转换过滤器:捕获微服务返回的
RpcException,转换为对应状态码的HTTP异常返回给客户端:
import { Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; import { BaseRpcExceptionFilter, RpcException } from '@nestjs/microservices'; import { Observable, throwError } from 'rxjs'; @Catch(RpcException) export class RpcToHttpExceptionFilter extends BaseRpcExceptionFilter { catch(exception: RpcException, host: ArgumentsHost) { const errorContent = exception.getError(); const statusCode = typeof errorContent === 'object' && 'status' in errorContent ? errorContent.status : HttpStatus.INTERNAL_SERVER_ERROR; return throwError(() => new HttpException(errorContent, statusCode)); } }
在网关调用微服务的接口上绑定该过滤器即可。
内容的提问来源于stack exchange,提问作者N30G4FF
相关产品推荐
相关产品推荐

