Skip to content

HasRateLimits on Request class throws "Call to undefined method send()" when 429 with sleep() is triggered #532

@MDDev31

Description

@MDDev31

Description

When using the HasRateLimits trait on a Request class (rather than a Connector), and configuring getTooManyAttemptsLimiter() with ->sleep(), a fatal error occurs when a 429 response is received.

The error happens at line 102 of HasRateLimits.php where $this->send() is called, but send() only exists on the Connector class, not on Request.

Why I Need Rate Limiting on Request

I need to define rate limits at the Request level because:

  1. Different endpoints have different rate limits - Each API endpoint has its own rate/burst values
  2. handleTooManyAttempts() uses request-specific data - When a 429 occurs, I calculate the wait time based on properties defined on the request (e.g., $rate, $burst)
// Each request has its own rate limit configuration
class GetOrderItems extends AbstractRequest
{
    protected float $rate = 0.5;    // 0.5 requests/second
    protected int $burst = 30;      // Burst of 30

    protected function handleTooManyAttempts(Response $response, Limit $limit): void
    {
        if ($response->status() !== 429) {
            return;
        }

        // Calculate wait time based on THIS request's rate/burst
        $secondsNeeded = (int) ceil($this->burst / $this->rate);

        $limit->exceeded(releaseInSeconds: $secondsNeeded);
    }
}

Moving the trait to the Connector would lose this per-request context.

Steps to Reproduce

  1. Add HasRateLimits trait to a Request class
  2. Override getTooManyAttemptsLimiter() with ->sleep() enabled
  3. Make a request that returns a 429 response

Minimal Example

  <?php

  use Saloon\Http\Request;
  use Saloon\Http\Response;
  use Saloon\RateLimitPlugin\Limit;
  use Saloon\RateLimitPlugin\Traits\HasRateLimits;
  use Saloon\RateLimitPlugin\Stores\MemoryStore;

  class MyRequest extends Request
  {
      use HasRateLimits;

      protected ?string $method = 'GET';

      // Request-specific rate limit values
      protected float $rate = 0.5;
      protected int $burst = 30;

      public function resolveEndpoint(): string
      {
          return '/api/resource';
      }

      protected function resolveLimits(): array
      {
          return [
              Limit::allow(10)->everyMinute()->sleep(),
          ];
      }

      protected function resolveRateLimitStore(): MemoryStore
      {
          return new MemoryStore();
      }

      protected function getTooManyAttemptsLimiter(): ?Limit
      {
          return Limit::custom($this->handleTooManyAttempts(...))->sleep();
      }

      protected function handleTooManyAttempts(Response $response, Limit $limit): void
      {
          if ($response->status() !== 429) {
              return;
          }

          // Uses request-specific properties
          $secondsNeeded = (int) ceil($this->burst / $this->rate);
          $limit->exceeded(releaseInSeconds: $secondsNeeded);
      }
  }

Expected Behavior

When a 429 response is received and ->sleep() is enabled, Saloon should wait and retry the request automatically.

Actual Behavior

Warning

Error: Call to undefined method MyRequest::send() at vendor/saloonphp/rate-limit-plugin/src/Traits/HasRateLimits.php:102

Root Cause

In HasRateLimits.php lines 94-102:

  if (isset($limitThatWasExceeded)) {
      if (! $limit->getShouldSleep()) {
          $this->throwLimitException($limitThatWasExceeded);
      }
      // This line fails when trait is on Request
      return $this->send($response->getRequest());
  }

When wasManuallyExceeded() is true and sleep() is enabled, the code calls $this->send() to retry. This works when the trait is on a Connector (which has send()), but fails when the trait is on a Request.

Environment

  • PHP: 8.3
  • Saloon: 3.x

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions