Documentation

RetryMiddleware
in package
implements MiddlewareInterface

Retry/backoff middleware - retries a request on transient network exceptions and/or specific response status codes, with exponential backoff and full jitter between attempts. Honors a Retry-After response header over the computed delay when present. Skips retrying entirely when the request has a non-seekable body, since a partially-consumed stream can't be safely resent.

Tags
category

Pop

author

Nick Sagona, III nick@popphp.org

copyright

Copyright (c) 2009-2026 Nick Sagona, III

license

https://www.popphp.org/license New BSD License

version
6.0.0

Table of Contents

Interfaces

MiddlewareInterface
Client middleware interface - a PSR-15-style interceptor for Client's outbound dispatch. Not a literal PSR-15 implementation: PSR-15's real interfaces are typed to ServerRequestInterface (server-side only) and cannot apply to a Client's outbound RequestInterface flow, so this mirrors PSR-15's shape with the request type swapped.

Properties

$baseDelay  : float
Base delay, in seconds, for the exponential backoff calculation
$maxDelay  : float
Maximum delay, in seconds, before jitter is applied
$maxRetries  : int
Maximum number of retries (attempts beyond the first try)
$onRetry  : callable|null
Optional observability callback, fired before each retry's sleep callable(int $attempt, RequestInterface $request, ?ResponseInterface $response, ?\Throwable $exception, float $delaySeconds): void
$retryableMethods  : array<string|int, mixed>
HTTP methods eligible for retry (uppercase)
$retryableStatusCodes  : array<string|int, mixed>
Response status codes eligible for retry
$shouldRetryException  : callable
Predicate deciding whether a caught exception is retryable callable(\Throwable $exception): bool
$sleeper  : callable
Sleep function, callable(float $seconds): void - defaults to a usleep() wrapper, overridable so tests don't have to actually sleep

Methods

__construct()  : mixed
Constructor
getBaseDelay()  : float
Get the base delay, in seconds
getMaxDelay()  : float
Get the maximum delay, in seconds
getMaxRetries()  : int
Get the maximum number of retries
getRetryableMethods()  : array<string|int, mixed>
Get the HTTP methods eligible for retry
getRetryableStatusCodes()  : array<string|int, mixed>
Get the response status codes eligible for retry
process()  : ResponseInterface
Process the request, retrying on transient failures per the configured policy
setBaseDelay()  : RetryMiddleware
Set the base delay, in seconds, for the exponential backoff calculation
setMaxDelay()  : RetryMiddleware
Set the maximum delay, in seconds, before jitter is applied
setMaxRetries()  : RetryMiddleware
Set the maximum number of retries
setOnRetry()  : RetryMiddleware
Set the observability callback, fired before each retry's sleep
setRetryableMethods()  : RetryMiddleware
Set the HTTP methods eligible for retry
setRetryableStatusCodes()  : RetryMiddleware
Set the response status codes eligible for retry
setShouldRetryException()  : RetryMiddleware
Set the predicate deciding whether a caught exception is retryable
setSleeper()  : RetryMiddleware
Set the sleep function - test-only override point, defaults to a usleep() wrapper
canRetryBody()  : bool
Determine if the request's body can be safely resent - true when there's no underlying stream to worry about, or the body is seekable.
computeDelay()  : float
Compute the delay before the next attempt - honors a Retry-After response header over the computed exponential-with-jitter delay when present, clamped to the configured maximum delay
isMethodRetryable()  : bool
Determine if the request's method is eligible for retry
parseRetryAfter()  : float|null
Parse a Retry-After header value - either an integer number of seconds, or an HTTP-date to wait until (RFC 9110)
rewindBody()  : void
Rewind the request's body, if it has an underlying stream. Uses the same "no stream attached" signal as canRetryBody() (see its docblock) so the two never drift out of sync.
wait()  : void
Compute the delay, fire the onRetry callback, sleep, and rewind the request body

Properties

$baseDelay

Base delay, in seconds, for the exponential backoff calculation

protected float $baseDelay = 0.1

$maxDelay

Maximum delay, in seconds, before jitter is applied

protected float $maxDelay = 10.0

$maxRetries

Maximum number of retries (attempts beyond the first try)

protected int $maxRetries = 3

$onRetry

Optional observability callback, fired before each retry's sleep callable(int $attempt, RequestInterface $request, ?ResponseInterface $response, ?\Throwable $exception, float $delaySeconds): void

protected callable|null $onRetry = null

$retryableMethods

HTTP methods eligible for retry (uppercase)

protected array<string|int, mixed> $retryableMethods = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS']

$retryableStatusCodes

Response status codes eligible for retry

protected array<string|int, mixed> $retryableStatusCodes = [429, 502, 503, 504]

$shouldRetryException

Predicate deciding whether a caught exception is retryable callable(\Throwable $exception): bool

protected callable $shouldRetryException

$sleeper

Sleep function, callable(float $seconds): void - defaults to a usleep() wrapper, overridable so tests don't have to actually sleep

protected callable $sleeper

Methods

__construct()

Constructor

public __construct([int $maxRetries = 3 ]) : mixed
Parameters
$maxRetries : int = 3

getBaseDelay()

Get the base delay, in seconds

public getBaseDelay() : float
Return values
float

getMaxDelay()

Get the maximum delay, in seconds

public getMaxDelay() : float
Return values
float

getMaxRetries()

Get the maximum number of retries

public getMaxRetries() : int
Return values
int

getRetryableMethods()

Get the HTTP methods eligible for retry

public getRetryableMethods() : array<string|int, mixed>
Return values
array<string|int, mixed>

getRetryableStatusCodes()

Get the response status codes eligible for retry

public getRetryableStatusCodes() : array<string|int, mixed>
Return values
array<string|int, mixed>

setOnRetry()

Set the observability callback, fired before each retry's sleep

public setOnRetry(callable $callback) : RetryMiddleware
Parameters
$callback : callable

callable(int $attempt, RequestInterface $request, ?ResponseInterface $response, ?\Throwable $exception, float $delaySeconds): void

Return values
RetryMiddleware

setRetryableMethods()

Set the HTTP methods eligible for retry

public setRetryableMethods(array<string|int, mixed> $methods) : RetryMiddleware
Parameters
$methods : array<string|int, mixed>
Return values
RetryMiddleware

setRetryableStatusCodes()

Set the response status codes eligible for retry

public setRetryableStatusCodes(array<string|int, mixed> $statusCodes) : RetryMiddleware
Parameters
$statusCodes : array<string|int, mixed>
Return values
RetryMiddleware

setShouldRetryException()

Set the predicate deciding whether a caught exception is retryable

public setShouldRetryException(callable $predicate) : RetryMiddleware
Parameters
$predicate : callable

callable(\Throwable $exception): bool

Return values
RetryMiddleware

setSleeper()

Set the sleep function - test-only override point, defaults to a usleep() wrapper

public setSleeper(callable $sleeper) : RetryMiddleware
Parameters
$sleeper : callable

callable(float $seconds): void

Return values
RetryMiddleware

canRetryBody()

Determine if the request's body can be safely resent - true when there's no underlying stream to worry about, or the body is seekable.

protected canRetryBody(RequestInterface $request) : bool

Deliberately does not use getSize() === 0 as the "no body" signal: a real, statable stream (e.g. a pipe, socket, or php://stdin) can legitimately report a size of 0 while still being non-rewindable, and that must fall through to the isSeekable() check rather than being waved through. A Body with no stream attached at all has no metadata (getMetadata() returns []), which is what actually means "no body".

Parameters
$request : RequestInterface
Return values
bool

computeDelay()

Compute the delay before the next attempt - honors a Retry-After response header over the computed exponential-with-jitter delay when present, clamped to the configured maximum delay

protected computeDelay(ResponseInterface|null $response, int $attempt) : float
Parameters
$response : ResponseInterface|null
$attempt : int
Return values
float

isMethodRetryable()

Determine if the request's method is eligible for retry

protected isMethodRetryable(RequestInterface $request) : bool
Parameters
$request : RequestInterface
Return values
bool

parseRetryAfter()

Parse a Retry-After header value - either an integer number of seconds, or an HTTP-date to wait until (RFC 9110)

protected parseRetryAfter(string $value) : float|null
Parameters
$value : string
Return values
float|null

rewindBody()

Rewind the request's body, if it has an underlying stream. Uses the same "no stream attached" signal as canRetryBody() (see its docblock) so the two never drift out of sync.

protected rewindBody(RequestInterface $request) : void
Parameters
$request : RequestInterface

wait()

Compute the delay, fire the onRetry callback, sleep, and rewind the request body

protected wait(RequestInterface $request, int $attempt, ResponseInterface|null $response, Throwable|null $exception) : void
Parameters
$request : RequestInterface
$attempt : int
$response : ResponseInterface|null
$exception : Throwable|null

        
On this page

Search results