Developer Tools

Retries

The package retries a request that failed for a reason worth retrying, and only when repeating it cannot apply the operation twice. It cannot be triggered on demand against PayMongo, so the example is a test that fakes a rate-limited response.

  • Any request is retried after a 429 Too Many Requests, or a connection that never opened (host not resolved, connection refused, TLS handshake failed). Both prove PayMongo did not act on it.
  • GET and DELETE requests, and a POST that carries an Idempotency-Key, are also retried after a 5xx or a timeout. The package adds that key automatically while PAYMONGO_AUTO_IDEMPOTENCY is on. A PUT, a PATCH, or a POST without a key is not, because PayMongo may have acted.
  • A request is retried up to 2 times after the first attempt (PAYMONGO_RETRIES). The first wait is up to 200 ms (PAYMONGO_RETRY_DELAY) and doubles on every retry, jittered so clients failing together spread out, never longer than 5000 ms (PAYMONGO_MAX_RETRY_DELAY).
  • When a 429 or 5xx carries a Retry-After header, the package waits that many seconds instead. A Retry-After longer than the maximum wait is not waited for.
  • A DELETE retried after a 5xx or a timeout that then gets a 404 counts as deleted: the attempt whose response was lost already removed the resource.
  • Once attempts run out, the last response surfaces as the mapped exception, such as RateLimitException, whose retryAfter holds the header's value.

A test proving a rate-limited request waits for Retry-After

PHP
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;
use Luigel\Paymongo\Facades\Paymongo;
use Luigel\Paymongo\Testing\Fixtures;

it('retries a rate-limited request after the Retry-After delay', function () {
    Sleep::fake();

    Paymongo::fake([
        'api.paymongo.com/v1/payments/*' => Http::sequence()
            ->push(['errors' => [['code' => 'rate_limited', 'detail' => 'Too many requests.']]], 429, ['Retry-After' => '2'])
            ->push(Fixtures::payment(['id' => 'pay_after_retry'])),
    ]);

    $payment = Paymongo::payments()->retrieve('pay_after_retry');

    expect($payment->id)->toBe('pay_after_retry');

    Sleep::assertSleptTimes(1);
    Sleep::assertSequence([Sleep::for(2)->seconds()]);
});