helpscout / helpscout-api-php

PHP Wrapper for the Help Scout API
MIT License
99 stars 62 forks source link

Enhancement: Better Rate Limit Handling & Exposing Rate Limit Headers #314

Closed vincentsch closed 10 months ago

vincentsch commented 1 year ago

The client doesn't effectively handle rate limit scenarios or make available important rate-limiting headers. Improved handling or increased transparency is vital given the rate limit restrictions present in APIs.

Current Behavior:

Upon exceeding the rate limit, the client throws a HelpScout\Api\Exception\RateLimitExceededException. However, it's challenging to discern when the rate limit will reset or gauge how close the client is to reaching the rate limit.

Desired Enhancements:

  1. Expose Rate Limit Headers: The Help Scout API provides essential headers related to rate limits:

    • X-RateLimit-Limit-Minute: Maximum number of requests per interval.
    • X-RateLimit-Remaining-Minute: Number of requests remaining in the current rate limit interval.
    • X-RateLimit-Retry-After: Time (in seconds) to wait until the rate limit refreshes.

    Making these headers accessible when using the client, particularly when handling the RateLimitExceededException, would be beneficial.

  2. Built-In Rate Limit Handling: Consider implementing built-in rate limit handling in the client. For instance, on encountering a rate limit error, the client could automatically wait (using the value from X-RateLimit-Retry-After) and then retry the request.

Workaround:

For developers currently facing this issue, here's a workaround to extract rate-limiting headers and retry the request:

try {
    $response = $client->conversations()->list(/* ... your request parameters ... */);
} catch (ClientException $exception) {
    // Extracting rate-limiting headers
    $limit = $exception->getResponse()->getHeader('X-RateLimit-Limit-Minute')[0];
    $remaining = $exception->getResponse()->getHeader('X-RateLimit-Remaining-Minute')[0];
    $retryAfter = $exception->getResponse()->getHeader('X-RateLimit-Retry-After')[0];

    echo "Limit: $limit, Remaining: $remaining, Retry After: $retryAfter seconds\n";

    // Sleeping for the required time before retrying
    sleep($retryAfter);

    // Attempt to retry the request
    try {
        $response = $client->conversations()->list(/* ... your request parameters ... */);
    } catch (Exception $retryException) {
        echo "Error after retry: " . $retryException->getMessage();
    }
}

This workaround is considered a temporary measure, and native support in the client wrapper would be the optimal solution.

Benefits:

vincentsch commented 1 year ago

Update

Here is the solution I am currently working with:

I added this method to my class:

protected function requestWithRateLimitHandling(callable $requestCallback)
    {
        while (true) {
            try {
                return $requestCallback();
            } catch (\HelpScout\Api\Exception\RateLimitExceededException $exception) {
                $limit = $exception->getResponse()->getHeader('X-RateLimit-Limit-Minute')[0];
                $remaining = $exception->getResponse()->getHeader('X-RateLimit-Remaining-Minute')[0];
                $retryAfter = $exception->getResponse()->getHeader('X-RateLimit-Retry-After')[0];

                $this->info("Rate limit reached. Limit: $limit, Remaining: $remaining, Retry After: $retryAfter seconds");

                sleep($retryAfter);
            } catch (\Exception $e) {
                $this->error($e->getMessage());
                return null;
            }
        }
    }

And then I wrap my actual requests like this:

$conversations = $this->requestWithRateLimitHandling(function () use ($filters, $request, $client) {
    return $client->conversations()->list($filters, $request);
});
$conversations = $this->requestWithRateLimitHandling(function () use ($conversations) {
    return $conversations->getNextPage();
});
miguelrs commented 10 months ago

@vincentsch Thanks a lot for opening this issue and giving so much information ❤️ We do understand your request and consider it a useful addition to the SDK 👍 We've added a card to our backlog of tickets for this, but I'm afraid we can't make any promises about whether/when we will be able to work on this. We will keep you updated if we make any progress. Thanks!