Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug] rate limit plugin not updated when rate limit exceeded #226

Closed
nicholaschiang opened this issue Mar 19, 2022 · 2 comments
Closed

[bug] rate limit plugin not updated when rate limit exceeded #226

nicholaschiang opened this issue Mar 19, 2022 · 2 comments
Labels
bug Something isn't working

Comments

@nicholaschiang
Copy link

nicholaschiang commented Mar 19, 2022

Describe the bug
A clear and concise description of what the bug is.

The @twitter-api-v2/plugin-rate-limit isn't updated (it's onAfterRequest isn't called) when a 429 rate limited exceeded error is thrown:

[dev:remix] [debug] Getting user (1329661759020363778) rate limit for: GET https://api.twitter.com/2/lists/:id/tweets
[dev:remix] [debug] Got user (1329661759020363778) rate limit (4/900 remaining until 3/18/2022, 9:50:30 PM) for: GET https://api.twitter.com/2/lists/:id/tweets
[dev:remix] [info] Fetching tweets for user (1329661759020363778) list (1498457571216134144)...
[dev:remix] [info] Fetching tweets for user (1329661759020363778) list (1504961420084932608)...
[dev:remix] ApiResponseError: Request failed with code 429
[dev:remix]     at RequestHandlerHelper.createResponseError (/home/nchiang/repos/tweetscape/node_modules/twitter-api-v2/dist/client-mixins/request-handler.helper.js:99:16)
[dev:remix]     at RequestHandlerHelper.onResponseEndHandler (/home/nchiang/repos/tweetscape/node_modules/twitter-api-v2/dist/client-mixins/request-handler.helper.js:227:25)
[dev:remix]     at Gunzip.emit (node:events:520:28)
[dev:remix]     at endReadableNT (node:internal/streams/readable:1346:12)
[dev:remix]     at processTicksAndRejections (node:internal/process/task_queues:83:21) {
[dev:remix]   error: true,
[dev:remix]   type: 'response',
[dev:remix]   code: 429,
[dev:remix]   headers: {
[dev:remix]     date: 'Sat, 19 Mar 2022 04:42:51 UTC',
[dev:remix]     server: 'tsa_a',
[dev:remix]     'set-cookie': [
[dev:remix]       'guest_id_marketing=v1%3A164766497161139951; Max-Age=63072000; Expires=Mon, 18 Mar 2024 04:42:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None',
[dev:remix]       'guest_id_ads=v1%3A164766497161139951; Max-Age=63072000; Expires=Mon, 18 Mar 2024 04:42:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None',
[dev:remix]       'personalization_id="v1_KY6eU2bfCl+kSIWvIk4w0g=="; Max-Age=63072000; Expires=Mon, 18 Mar 2024 04:42:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None',
[dev:remix]       'guest_id=v1%3A164766497161139951; Max-Age=63072000; Expires=Mon, 18 Mar 2024 04:42:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None'
[dev:remix]     ],
[dev:remix]     'api-version': '2.38',
[dev:remix]     'content-type': 'application/json; charset=utf-8',
[dev:remix]     'cache-control': 'no-cache, no-store, max-age=0',
[dev:remix]     'content-length': '94',
[dev:remix]     'x-access-level': 'read-write',
[dev:remix]     'x-frame-options': 'SAMEORIGIN',
[dev:remix]     'content-encoding': 'gzip',
[dev:remix]     'x-xss-protection': '0',
[dev:remix]     'x-rate-limit-limit': '900',
[dev:remix]     'x-rate-limit-reset': '1647665430',
[dev:remix]     'content-disposition': 'attachment; filename=json.json',
[dev:remix]     'x-content-type-options': 'nosniff',
[dev:remix]     'x-rate-limit-remaining': '0',
[dev:remix]     'strict-transport-security': 'max-age=631138519',
[dev:remix]     'x-response-time': '17',
[dev:remix]     'x-connection-hash': 'ee7578f25fe7274fe28e9ef538a9e9fd2789b287ee4da70e81721226ad7dfd34',
[dev:remix]     connection: 'close'
[dev:remix]   },
[dev:remix]   rateLimit: { limit: 900, remaining: 0, reset: 1647665430 },
[dev:remix]   data: {
[dev:remix]     title: 'Too Many Requests',
[dev:remix]     detail: 'Too Many Requests',
[dev:remix]     type: 'about:blank',
[dev:remix]     status: 429
[dev:remix]   }
[dev:remix] }
[dev:remix] GET /sync/tweets?_data=routes%2Fsync%2Ftweets 500 - - 376.999 ms

To Reproduce
Please indicate all steps that lead to this bug:

  1. Run a bunch of API requests to overflow their rate limits.
  2. Notice that an error is thrown but the TwitterApiRateLimitMemoryStore#set method is never called.

Expected behavior
A clear and concise description of what you expected to happen.

When a rate limit is exceeded, the @twitter-api-v2/plugin-rate-limit should update the stored rate limit so as to handled edge-case race conditions (e.g. when making 100s of requests per sec on serverless functions where there may be race conditions when interacting with a shared PostgreSQL rate limit store).

Version

  • Node.js version: 16.14.0 LTS
  • Lib version: 1.11.0
  • OS (especially if you use Windows): Pop_OS! 20.04 (Ubuntu 20.04 LTS)

Additional context
Add any other context about the problem here.

Here's my custom store for the rate limit plugin for this project:

// app/limit.server.ts
import type {
  ITwitterApiRateLimitGetArgs,
  ITwitterApiRateLimitSetArgs,
  ITwitterApiRateLimitStore,
} from '@twitter-api-v2/plugin-rate-limit';
import { TwitterApiRateLimitMemoryStore } from '@twitter-api-v2/plugin-rate-limit';
import type { TwitterRateLimit } from 'twitter-api-v2';

import { db } from '~/db.server';
import { log } from '~/utils.server';

function limitToString(limit: TwitterRateLimit): string {
  return `rate limit (${limit.remaining}/${limit.limit} remaining until ${new Date(limit.reset * 1000).toLocaleString()})`;
}

export class TwitterApiRateLimitDBStore implements ITwitterApiRateLimitStore {
  private memoryStore = new TwitterApiRateLimitMemoryStore();

  public constructor(private uid: string) {}

  public async get(args: ITwitterApiRateLimitGetArgs) {
    log.debug(`Getting user (${this.uid}) rate limit for: ${args.method ?? 'GET'} ${args.endpoint}`);
    if (args.method) {
      const fromMemory = this.memoryStore.get(args);
      if (fromMemory) {
        log.debug(`Got user (${this.uid}) ${limitToString(fromMemory)} for: ${args.method} ${args.endpoint}`);
        return fromMemory;
      }
      const fromDB = await db.limits.findFirst({
          where: {
            influencer_id: this.uid,
            method: args.method,
            endpoint: args.endpoint,
            resets_at: { gt: new Date() },
          },
        });
      if (fromDB) {
        log.debug(`Got user (${this.uid}) ${limitToString(fromDB)} for: ${args.method} ${args.endpoint}`);
        return fromDB;
      }
    }
    const fromMemory = this.memoryStore.get(args);
    if (fromMemory) {
      log.debug(`Got user (${this.uid}) ${limitToString(fromMemory)} for: GET ${args.endpoint}`);
      return fromMemory;
    }
    const fromDB = await db.limits.findFirst({
        where: {
          influencer_id: this.uid,
          endpoint: args.endpoint,
          resets_at: { gt: new Date() },
        },
      });
    if (fromDB) {
      log.debug(`Got user (${this.uid}) ${limitToString(fromDB)} for: GET ${args.endpoint}`);
      return fromDB;
    }
    log.debug(`Could not find non-expired rate limit for user (${this.uid}) and: ${args.method ?? 'GET'} ${args.endpoint}`);
  }

  public async set(args: ITwitterApiRateLimitSetArgs) {
    const limit = {
      ...args.rateLimit,
      method: args.method,
      endpoint: args.endpoint,
      influencer_id: this.uid,
      resets_at: new Date(args.rateLimit.reset * 1000),
    };
    log.debug(
      `Setting user (${this.uid}) ${limitToString(limit)} for: ${args.method} ${args.endpoint}`
    );
    this.memoryStore.set(args);
    await db.limits.upsert({
      create: limit,
      update: limit,
      where: {
        influencer_id_method_endpoint: {
          influencer_id: limit.influencer_id,
          endpoint: limit.endpoint,
          method: limit.method,
        },
      },
    });
  }

  public async delete(method: string, endpoint: string) {
    log.debug(
      `Deleting user (${this.uid}) rate limit for: ${method} ${endpoint}`
    );
    this.memoryStore.delete(method, endpoint);
    await db.limits.delete({
      where: {
        influencer_id_method_endpoint: {
          method,
          endpoint,
          influencer_id: this.uid,
        },
      },
    });
  }
}
@alkihis alkihis added the bug Something isn't working label Mar 19, 2022
@alkihis
Copy link
Collaborator

alkihis commented Mar 19, 2022

Hi :)
Should be fixed by upgrading twitter-api-v2 to 1.11.1 and @twitter-api-v2/plugin-rate-limit to 1.1.0 :)

Tells me if its ok for you.

@nicholaschiang
Copy link
Author

Now I"m running into #229... 😭

@alkihis alkihis closed this as completed Mar 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants