Utopia\Client\Decorator\Retry is an adapter that decorates another adapter and
retries failed requests. Because it is itself an Adapter, it composes — wrap it in
Client (and stack it with other decorators) wherever a transport goes.
<?php
use Utopia\Client;
use Utopia\Client\Adapter\Curl\Client as CurlAdapter;
use Utopia\Client\Decorator\Retry;
$client = new Client(new Retry(new CurlAdapter()));With no configuration it uses Backoff, the default best-practice strategy:
- Retries only idempotent methods (
GET,HEAD,PUT,DELETE,OPTIONS,TRACE), so a lost response can't cause a double-submit. - Retries only transient failures: transport errors (
NetworkExceptionInterface— DNS, timeout, connection reset, TLS) and overloaded responses (429,502,503,504). - Waits with exponential backoff and full jitter, honouring a numeric
Retry-Afterheader when present. - Makes up to 3 attempts total.
RequestExceptionInterface failures (a bad URI, a malformed response) are never
retried — see error handling for that distinction. Streaming
requests are retried only when the failure occurs before any byte reaches the sink,
since replaying would duplicate delivered data.
Backoff is configurable through its constructor:
<?php
use Utopia\Client\Decorator\Retry;
use Utopia\Client\Decorator\Retry\Backoff;
$client = new Client(
new Retry(
new CurlAdapter(),
new Backoff(
maxAttempts: 5,
baseDelay: 0.2, // seconds; first retry waits up to baseDelay
maxDelay: 30.0, // ceiling for any single wait
multiplier: 2.0, // delay ceiling grows by this factor each attempt
),
),
);Every retry decision lives behind one interface, so a custom policy is a single
method. Return the number of seconds to wait, or null to stop and surface the
outcome:
<?php
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Utopia\Client\Decorator\Retry;
use Utopia\Client\Decorator\Retry\Strategy;
final class RetryOnceImmediately implements Strategy
{
public function delay(RequestInterface $request, int $attempt, ?ResponseInterface $response, ?ClientExceptionInterface $error): ?float
{
return $attempt === 1 && $error !== null ? 0.0 : null;
}
}
$client = new Client(new Retry(new CurlAdapter(), new RetryOnceImmediately()));Exactly one of $response or $error is non-null: $response when the inner
adapter returned (including 4xx/5xx), $error when it threw.
The retry decorator sleeps between attempts with usleep() by default. With Swoole's
runtime coroutine hooks enabled (Swoole\Runtime::enableCoroutine(), which most
Swoole servers turn on), usleep() is hooked to yield to the scheduler instead of
blocking, so no special configuration is needed.
If you run without those hooks, inject a coroutine-aware sleeper:
<?php
use Swoole\Coroutine;
use Utopia\Client\Decorator\Retry;
$retry = new Retry(
$swooleAdapter,
sleep: static fn (float $seconds) => Coroutine::sleep($seconds),
);Retry extends Utopia\Client\Decorator, the base for any adapter that wraps
another. It forwards every configuration helper to the inner adapter and delegates
sending; a subclass overrides only sendRequest() / stream(). Because each
decorator is itself an Adapter, they stack in any order — for example
new Client(new Retry(new SomeOtherDecorator(new CurlAdapter()))).