#Throttler doesn't work with nested GraphQL resolvers

11 messages · Page 1 of 1 (latest)

scenic leaf
toxic patio
#

I think the error text is pretty clear. The question is then, are you only trying to throttle a lower level of your graph? Or, do you wish to throttle the endpoint?

scenic leaf
#

@toxic patio The result I'm trying to achieve is - to have a global throttle config and be able to fine-tune it for specific "namespaces" or even queries

toxic patio
#

So, have you gotten the throttler working globally?

toxic patio
#

Ok. I looked at your repo and can see you have the global rate limiter working. Let me look further.

#

And, it was very early with little coffee in my brain, when I read your post. I thought your issue was an error text. LOL! 😄

#

I believe a better solution is to have a "global to the resolver" guard, which basically looks into the request and the query and figures out what is being asked for and throttles the request accordingly. This will also simplify setting up the throttling rules for each resolver class. I asked AI to help me with an example, and it spat out this code for me (for example purposes only, not tested):

#

GqlThrottlerGuard (for a particular resolver)

import { Injectable, ExecutionContext } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';
import { GqlExecutionContext } from '@nestjs/graphql';
import { GraphQLResolveInfo } from 'graphql';

@Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
  getRequestResponse(context: ExecutionContext) {
    const gqlCtx = GqlExecutionContext.create(context);
    const ctx = gqlCtx.getContext();
    return { req: ctx.req, res: ctx.res };
  }

  async handleRequest(
    context: ExecutionContext,
    limit: number,
    ttl: number,
  ): Promise<boolean> {
    const gqlCtx = GqlExecutionContext.create(context);
    const info = gqlCtx.getInfo<GraphQLResolveInfo>();

    // Extract the operation name
    const operationName = info.operation.name?.value;

    // Define which queries to throttle
    const queriesToThrottle = ['HighlyExpensiveQuery', 'AnotherSlowQuery'];

    // Check if the current operation name is in the list of queries to throttle
    if (operationName && queriesToThrottle.includes(operationName)) {
      // Apply throttling for these specific queries
      const httpContext = context.switchToHttp();
      const request = httpContext.getRequest();

      // You might need to customize the tracker for GraphQL requests
      // For example, using the operation name and user ID
      const tracker = this.getTracker(request); // Implement or override getTracker if needed

      const key = this.generateKey(context, tracker, limit, ttl);
      const { totalHits } = await this.storageService.increment(key, ttl);

      if (totalHits > limit) {
        this.throwThrottlingException(context);
      }

      return true; // Allow the request to proceed if within limits
    }

    // For all other GraphQL operations, skip throttling
    return true;
  }
}
#

MyResolver (using the GqlThrottlerGuard)

import { Resolver, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { GqlThrottlerGuard } from './gql-throttler.guard'; // Adjust the import path
import { Throttle } from '@nestjs/throttler';

@Resolver()
@UseGuards(GqlThrottlerGuard) // Apply guard to all methods in this resolver
export class MyResolver {
  @Query(() => String)
  async nonThrottledQuery(): Promise<string> {
    return 'This query is not throttled (if guard is applied globally and not explicitly throttled or skipped).';
  }

  @Query(() => String)
  @Throttle({ default: { limit: 5, ttl: 30000 } }) // Override global or apply specific throttle
  async highlyExpensiveQuery(): Promise<string> {
    return 'This query is throttled to 5 requests per 30 seconds.';
  }

  @Query(() => String)
  @Throttle({ default: { limit: 2, ttl: 10000 } }) // Another throttled query
  async anotherSlowQuery(): Promise<string> {
    return 'This query is throttled to 2 requests per 10 seconds.';
  }
}
scenic leaf
#

Hi @toxic patio,
Yeah, coffee is a must)

Regarding the AI example, beside the small issues like params of handleRequest (which is easy to adjust), the example is missing the main thing - nested resolver - basically where the problem is for me.

For the structure like in the AI example - no need to invent the wheel - throttler works as expected, the problem raises when we want to rate-limit a nested resolver's method (in my example authors => getOne)

Just to summarise:

  • ✅ global - works as expected
  • ✅ root level resolver (ex: authors) - works as expected (I'm able to override the global)
  • ‼️ nested resolver's methods - here is the problem (uses the global one)
scenic leaf
#

@toxic patio just pinging you if you have any ideas. Thanks anyway for your time.