Retry Handling
The Fetch PHP package includes built-in retry functionality to handle transient failures gracefully. This guide explains how to configure and use the retry mechanism.
Basic Retry Configuration
You can configure retries using the retry()
method on the ClientHandler:
use Fetch\Http\ClientHandler;
$response = ClientHandler::create()
->retry(3, 100) // Retry up to 3 times with initial delay of 100ms
->get('https://api.example.com/unstable-endpoint');
Using helper functions:
$response = fetch('https://api.example.com/unstable-endpoint', [
'retries' => 3, // Retry up to 3 times
'retry_delay' => 100 // Initial delay of 100ms
]);
How Retry Works
When a request fails due to a retryable error (network issues or certain HTTP status codes), the package will:
- Wait for a specified delay
- Apply exponential backoff with jitter (randomness)
- Retry the request
- Repeat until success or the maximum retry count is reached
The delay increases exponentially with each retry attempt:
- First retry: Initial delay (e.g., 100ms)
- Second retry: ~2x initial delay + jitter
- Third retry: ~4x initial delay + jitter
- And so on...
The jitter (random variation) helps prevent multiple clients from retrying simultaneously, which can worsen outages.
Using Type-Safe Enums with Retries
You can use the Status
enum for more type-safe retry configuration:
use Fetch\Http\ClientHandler;
use Fetch\Enum\Status;
$response = ClientHandler::create()
->retry(3, 100)
->retryStatusCodes([
Status::TOO_MANY_REQUESTS->value,
Status::SERVICE_UNAVAILABLE->value,
Status::GATEWAY_TIMEOUT->value
])
->get('https://api.example.com/unstable-endpoint');
Customizing Retryable Status Codes
By default, the client retries on these HTTP status codes:
- 408 (Request Timeout)
- 429 (Too Many Requests)
- 500, 502, 503, 504 (Server Errors)
- And several other common error codes
You can customize which status codes should trigger retries:
$client = ClientHandler::create()
->retry(3, 100)
->retryStatusCodes([429, 503, 504]); // Only retry on these status codes
$response = $client->get('https://api.example.com/unstable-endpoint');
Customizing Retryable Exceptions
By default, the client retries on network-related exceptions like ConnectException
. You can customize which exception types should trigger retries:
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
$client = ClientHandler::create()
->retry(3, 100)
->retryExceptions([
ConnectException::class,
RequestException::class
]);
$response = $client->get('https://api.example.com/unstable-endpoint');
Checking Retry Configuration
You can check the current retry configuration:
$client = ClientHandler::create()->retry(3, 200);
$maxRetries = $client->getMaxRetries(); // 3
$retryDelay = $client->getRetryDelay(); // 200
$statusCodes = $client->getRetryableStatusCodes(); // Array of status codes
$exceptions = $client->getRetryableExceptions(); // Array of exception classes
Global Retry Configuration
You can set up global retry settings that apply to all requests:
// Configure global retry settings
fetch_client([
'retries' => 3,
'retry_delay' => 100
]);
// All requests will now use these retry settings
$response = fetch('https://api.example.com/users');
Logging Retries
If you've set up a logger, retry attempts will be automatically logged:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fetch\Http\ClientHandler;
// Create a logger
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO));
// Create a client with logging and retries
$client = ClientHandler::create();
$client->setLogger($logger);
$client->retry(3, 100);
// Send a request that might require retries
$response = $client->get('https://api.example.com/unstable-endpoint');
// Retry attempts will be logged to logs/http.log
A typical retry log entry looks like:
[2023-09-15 14:30:12] http.INFO: Retrying request {"attempt":1,"max_attempts":3,"uri":"https://api.example.com/unstable-endpoint","method":"GET","error":"Connection timed out","code":28}
Asynchronous Retries
Retries also work with asynchronous requests:
use function async;
use function await;
use function retry;
// Retry asynchronous operations
$result = await(retry(
function() {
return async(function() {
return fetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attempts
function($attempt) {
// Exponential backoff strategy
return min(pow(2, $attempt) * 100, 1000);
}
));
// Process the result
$data = $result->json();
Real-World Examples
Handling Rate Limits
APIs often implement rate limiting. You can configure your client to automatically retry when hitting rate limits:
use Fetch\Enum\Status;
$client = ClientHandler::create()
->retry(3, 1000) // Longer initial delay for rate limits
->retryStatusCodes([Status::TOO_MANY_REQUESTS->value]) // Only retry on Too Many Requests
->get('https://api.example.com/rate-limited-endpoint');
Handling Network Instability
For unreliable network connections:
$client = ClientHandler::create()
->retry(5, 200) // More retries with moderate delay
// Using default retryable status codes and exceptions
->get('https://api.example.com/endpoint');
Handling Server Maintenance
For APIs that might be temporarily down for maintenance:
use Fetch\Enum\Status;
$client = ClientHandler::create()
->retry(10, 5000) // Many retries with long delay (5 seconds)
->retryStatusCodes([Status::SERVICE_UNAVAILABLE->value]) // Service Unavailable
->get('https://api.example.com/endpoint');
Combining Retry with Timeout
You can combine retry logic with timeout settings:
$client = ClientHandler::create()
->timeout(5) // 5 second timeout for each attempt
->retry(3, 100) // 3 retries with 100ms initial delay
->get('https://api.example.com/endpoint');
Implementing Advanced Retry Logic
For more complex scenarios, you can implement custom retry logic:
use Fetch\Http\ClientHandler;
use Fetch\Http\Response;
use GuzzleHttp\Exception\RequestException;
use Fetch\Enum\Status;
function makeRequestWithCustomRetry(string $url, int $maxAttempts = 3): Response {
$attempt = 0;
while (true) {
try {
$client = ClientHandler::create();
$response = $client->get($url);
// Check if we got a success response
if ($response->successful()) {
return $response;
}
// Handle specific status codes
if ($response->statusEnum() === Status::TOO_MANY_REQUESTS) {
// Get retry-after header if available
$retryAfter = $response->header('Retry-After');
$delay = $retryAfter ? (int) $retryAfter * 1000 : 1000;
} else {
// Otherwise use exponential backoff
$delay = 100 * (2 ** $attempt);
}
// Add some jitter (±20%)
$jitter = mt_rand(-20, 20) / 100;
$delay = (int) ($delay * (1 + $jitter));
$attempt++;
// Check if we've exceeded max attempts
if ($attempt >= $maxAttempts) {
return $response; // Return the last response
}
// Wait before retrying
usleep($delay * 1000); // Convert ms to μs
} catch (RequestException $e) {
$attempt++;
// Check if we've exceeded max attempts
if ($attempt >= $maxAttempts) {
throw $e; // Rethrow the last exception
}
// Wait before retrying
$delay = 100 * (2 ** $attempt);
usleep($delay * 1000);
}
}
}
// Use the custom retry function
$response = makeRequestWithCustomRetry('https://api.example.com/users');
Monitoring Retry Activity
To monitor retry activity, you can combine logging with a custom callback:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fetch\Http\ClientHandler;
// Create a logger
$logger = new Logger('retry');
$logger->pushHandler(new StreamHandler('logs/retry.log', Logger::INFO));
// Create a client with the logger
$client = ClientHandler::create();
$client->setLogger($logger);
$client->retry(3, 100);
// Make the request
$response = $client->get('https://api.example.com/unstable-endpoint');
// After the request completes, you can get debug info
$debugInfo = $client->debug();
echo "Request required " . $debugInfo['retries'] . " retries\n";
Best Practices
Use Type-Safe Enums: Leverage the Status enum for clearer and safer code when configuring retryable status codes.
Start with Conservative Settings: Begin with a small number of retries (2-3) and moderate delays (100-200ms) and adjust based on your needs.
Be Mindful of Server Load: Excessive retries can amplify problems during outages. Be respectful of the services you're calling.
Use Appropriate Timeout Values: Set reasonable timeouts in conjunction with retries to avoid long-running requests.
Limit Retryable Status Codes: Only retry on status codes that indicate transient issues. Don't retry on client errors like 400, 401, or 404.
Monitor Retry Activity: Log retry attempts to identify recurring issues with specific endpoints.
Consider Retry-After Headers: For rate limiting (429), respect the Retry-After header if provided by the server.
Add Jitter: The built-in retry mechanism includes jitter, which helps prevent "thundering herd" problems.
Combine with Logging: Always add logging when using retries to track and debug retry patterns.
Use Async Retries for Parallel Operations: When working with async code, use the retry function for better integration with the async/await pattern.
Next Steps
- Learn about Error Handling for comprehensive error management
- Explore Logging for monitoring request and retry activity
- See Authentication for handling authentication errors and retries
- Check out Asynchronous Requests for integrating retries with async operations