You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何仅对成功请求应用Throttle限流?

问题描述

需要实现仅在请求成功(状态码200-399)时触发限流,且支持按路由单独配置规则。具体场景:短信验证码路由(注册/找回密码场景)需限制每分钟1次请求,但参数验证不通过时(如返回400状态码)不应触发限流。

尝试过两种方案但均失效:

  1. 在ThrottlerModule的skipIf中判断响应状态码,但获取到的状态码始终为200
ThrottlerModule.forRoot({
    throttlers: [ { ttl: 0, limit: 0 } ],
    skipIf(context)
    {
        const response = context.switchToHttp().getResponse();
        return response.statusCode < 200 || response.statusCode >= 400;
    }
}),
  1. 自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 03:26:17