skip to Main Content

I was wondering if we can achieve Rate Limiting of IP and Request Body (same some username) separately (Either one if fulfilled should give me Error 429) for same controller function (Route).

Tried Using following packages

"nestjs-throttler-storage-redis": "^0.3.0"
"@nestjs/throttler": "^4.0.0",
"ioredis": "^5.3.2"

app.module.ts

ThrottlerModule.forRoot({
  ttl: process.env.IP_VELOCITY_TTL as unknown as number, // 24 hours in seconds
  limit: process.env.IP_VELOCITY_COUNT as unknown as number, // X number requests per ttl per key (IP address in this case)
  storage: new ThrottlerStorageRedisService(new Redis()),
}),

In Respective Module.ts

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard,
},

controller.ts

@Throttle(3, 60 * 60)

But this is not sufficient as this is blocking all the requests post 3 times!

Can anybody suggest me to achieve this in Right way ?

2

Answers


  1. Chosen as BEST ANSWER

    The trick was to overwrite ThrottlerGuard Class like below -

    import { ExecutionContext, Injectable } from '@nestjs/common';
    import { ThrottlerGuard } from '@nestjs/throttler';
    
    @Injectable()
    export class CustomThrottlerGuard extends ThrottlerGuard {
      // Overwritten to handle the IP restriction along with firstName + lastName restriction
      async handleRequest(
        context: ExecutionContext,
        limit: number,
        ttl: number
      ): Promise<boolean> {
        
        const { req, res } = this.getRequestResponse(context);
    
        // Return early if the current user agent should be ignored.
        if (Array.isArray(this.options.ignoreUserAgents)) {
          for (const pattern of this.options.ignoreUserAgents) {
            if (pattern.test(req.headers['user-agent'])) {
              return true;
            }
          }
        }
    
        // Tracker for IP
        const tracker = this.getTracker(req);
        const key = this.generateKey(context, tracker);
        const { totalHits, timeToExpire } = await this.storageService.increment(
          key,
          ttl
        );
    
        // Tracker for firstName and lastName
        const firstNameAndLastNameTracker = this.getNameTracker(req);
        const nameKey = this.generateKey(context, firstNameAndLastNameTracker);
        const { totalHits: totalHitsName, timeToExpire: timeToExpireName } =
          await this.storageService.increment(nameKey, ttl);
    
        // Throw an error when the user reached their limit (IP).
        if (totalHits > limit) {
          res.header('Retry-After', timeToExpire);
          this.throwThrottlingException(context);
        }
    
        // Throw an Error when user reached their firstName + lastName Limit.
        if (
          totalHitsName > parseInt(process.env.FIRSTNAME_LASTNAME_MAX_TRY_COUNT)
        ) {
          res.header('Retry-After', timeToExpireName);
          this.throwThrottlingException(context);
        }
    
        res.header(`${this.headerPrefix}-Limit`, limit);
        // We're about to add a record so we need to take that into account here.
        // Otherwise the header says we have a request left when there are none.
        res.header(
          `${this.headerPrefix}-Remaining`,
          Math.max(0, limit - totalHits)
        );
        res.header(`${this.headerPrefix}-Reset`, timeToExpire);
    
        return true;
      }
    
      protected getNameTracker(req: Record<string, any>): string {
        return req.body.firstName + req.body.lastName;
      }
    }
    

  2. You’ll need to create your own guard that extends ThrottlerGuard and overrides the getTracker method so that it returns this ip and req.body.username combo. Something like

    @Injectable()
    export class ThrottleIpBodyGuard extends ThrottlerGuard {
    
      getTracker(req: Request) {
        return req.ip + req.body.username;
      }
    }
    

    Then, instead of useClass: ThrottlerGuard you can use useClass: ThrottleIpBodyGuard

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search