如何仅对成功请求应用Throttle限流?
问题描述
需要实现仅在请求成功(状态码200-399)时触发限流,且支持按路由单独配置规则。具体场景:短信验证码路由(注册/找回密码场景)需限制每分钟1次请求,但参数验证不通过时(如返回400状态码)不应触发限流。
尝试过两种方案但均失效:
- 在
ThrottlerModule的skipIf中判断响应状态码,但获取到的状态码始终为200
ThrottlerModule.forRoot({ throttlers: [ { ttl: 0, limit: 0 } ], skipIf(context) { const response = context.switchToHttp().getResponse(); return response.statusCode < 200 || response.statusCode >= 400; } }),
- 自定义
SuccessThrottlingGuard继承ThrottlerGuard,结果同样无法正确获取实际响应状态码
import { ExecutionContext, Injectable } from "@nestjs/common"; import { ThrottlerGuard } from "@nestjs/throttler"; @Injectable() export class SuccessThrottlingGuard extends ThrottlerGuard { async canActivate(context: ExecutionContext): Promise<boolean> { const response = context.switchToHttp().getResponse(); if (response.statusCode >= 200 && response.statusCode < 400) { return super.canActivate(context); } return true; } }
问题原因
上述方案失效的核心原因:ThrottlerGuard的canActivate方法和skipIf回调都在请求处理前执行,此时业务逻辑尚未运行,响应对象的statusCode仍为默认值200,无法获取到实际的响应状态码。
解决方案
通过拦截器+ThrottlerService组合实现:让限流检查仍由ThrottlerGuard完成,将限流计数的逻辑移到请求处理完成后,根据实际响应状态码决定是否记录计数。
步骤1:自定义限流拦截器
该拦截器在请求处理完成后(响应发送前)判断状态码,仅当请求成功时调用ThrottlerService记录限流计数:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { ThrottlerService } from '@nestjs/throttler'; @Injectable() export class SuccessOnlyThrottleInterceptor implements NestInterceptor { constructor(private readonly throttlerService: ThrottlerService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); // 生成与ThrottlerGuard一致的限流标识key const throttleKey = this.throttlerService.generateKey(request); return next.handle().pipe( tap(() => { // 仅当响应状态码为200-399时,记录限流计数 if (response.statusCode >= 200 && response.statusCode < 400) { this.throttlerService.record(throttleKey); } }), ); } }
步骤2:配置ThrottlerModule
全局配置中设置skipIf为true,禁用Guard默认的计数逻辑,让拦截器接管计数:
import { ThrottlerModule } from '@nestjs/throttler'; @Module({ imports: [ ThrottlerModule.forRoot({ throttlers: [{ ttl: 60000, limit: 1 }], // 默认全局规则:每分钟1次 skipIf: () => true, // 让Guard跳过默认计数 }), ], }) export class AppModule {}
步骤3:在目标路由上使用
在需要限流的路由上,同时启用ThrottlerGuard(负责检查限流)和自定义拦截器(负责记录成功请求的计数),也可通过@Throttle()装饰器单独配置路由规则:
import { Controller, Post, UseGuards, UseInterceptors, Body } from '@nestjs/common'; import { ThrottlerGuard, Throttle } from '@nestjs/throttler'; import { SuccessOnlyThrottleInterceptor } from './success-only-throttle.interceptor'; import { SmsDto } from './sms.dto'; @Controller('auth') export class AuthController { @Post('send-register-sms') @UseGuards(ThrottlerGuard) @UseInterceptors(SuccessOnlyThrottleInterceptor) @Throttle({ limit: 1, ttl: 60000 }) // 该路由单独配置:每分钟1次 async sendRegisterSms(@Body() body: SmsDto) { // 参数验证逻辑:失败会抛出BadRequestException(400),此时拦截器不会计数 // 业务逻辑:成功返回200,拦截器记录计数 return { message: '注册验证码已发送' }; } @Post('send-reset-pwd-sms') @UseGuards(ThrottlerGuard) @UseInterceptors(SuccessOnlyThrottleInterceptor) @Throttle({ limit: 1, ttl: 60000 }) async sendResetPwdSms(@Body() body: SmsDto) { return { message: '找回密码验证码已发送' }; } }
效果说明
- 参数验证失败(返回400等错误状态码)时,拦截器不会调用
record方法,不会消耗限流次数 - 请求成功(返回200-399)时,拦截器记录计数,触发限流规则
内容的提问来源于stack exchange,提问作者Oliver Patterson
相关产品推荐
相关产品推荐

