Error Handling
This guide explains how to handle errors when making HTTP requests with the Fetch HTTP package.
Response Status Checking
The most common way to handle HTTP errors is by checking the response status:
use function Fetch\Http\get;
$response = get('https://api.example.com/users/123');
if ($response->successful()) {
// Status code is 2xx - process the response
$user = $response->json();
echo "Found user: {$user['name']}";
} else {
// Error occurred - handle based on status code
echo "Error: " . $response->status() . " " . $response->statusText();
}
Status Category Methods
The Response
class provides methods to check different status code categories:
$response = get('https://api.example.com/users/123');
if ($response->successful()) {
// Status code is 2xx
$user = $response->json();
echo "Found user: {$user['name']}";
} elseif ($response->clientError()) {
// Status code is 4xx
echo "Client error: " . $response->status();
} elseif ($response->serverError()) {
// Status code is 5xx
echo "Server error: " . $response->status();
} elseif ($response->redirect()) {
// Status code is 3xx
echo "Redirect to: " . $response->header('Location');
}
Specific Status Code Methods
For handling specific status codes, the Response
class provides dedicated methods:
if ($response->isOk()) {
// 200 OK
$data = $response->json();
} elseif ($response->isNotFound()) {
// 404 Not Found
echo "Resource not found";
} elseif ($response->isUnauthorized()) {
// 401 Unauthorized
echo "Authentication required";
} elseif ($response->isForbidden()) {
// 403 Forbidden
echo "Access denied";
} elseif ($response->isUnprocessableEntity()) {
// 422 Unprocessable Entity
$errors = $response->json()['errors'] ?? [];
foreach ($errors as $field => $messages) {
echo "{$field}: " . implode(', ', $messages) . "\n";
}
}
Exception Handling
When network errors or other exceptions occur, they are thrown as PHP exceptions:
use Fetch\Exceptions\NetworkException;
use Fetch\Exceptions\RequestException;
use Fetch\Exceptions\ClientException;
use Fetch\Exceptions\TimeoutException;
try {
$response = get('https://api.example.com/users');
if ($response->failed()) {
throw new \Exception("Request failed with status: " . $response->status());
}
$users = $response->json();
} catch (NetworkException $e) {
// Network-related issues (DNS failure, connection refused, etc.)
echo "Network error: " . $e->getMessage();
} catch (TimeoutException $e) {
// Request timed out
echo "Request timed out: " . $e->getMessage();
} catch (RequestException $e) {
// HTTP request errors
echo "Request error: " . $e->getMessage();
// If the exception has a response, you can still access it
if ($e->hasResponse()) {
$errorResponse = $e->getResponse();
$statusCode = $errorResponse->status();
$errorDetails = $errorResponse->json()['error'] ?? 'Unknown error';
echo "Status: {$statusCode}, Error: {$errorDetails}";
}
} catch (ClientException $e) {
// General client errors
echo "Client error: " . $e->getMessage();
} catch (\Exception $e) {
// Catch any other exceptions
echo "Error: " . $e->getMessage();
}
Handling JSON Decoding Errors
When decoding JSON responses, you may encounter parsing errors:
try {
$data = $response->json();
} catch (\RuntimeException $e) {
// JSON parsing failed
echo "Failed to decode JSON: " . $e->getMessage();
// You can access the raw response body
$rawBody = $response->body();
echo "Raw response: " . $rawBody;
}
To suppress JSON decoding errors:
// Pass false to disable throwing exceptions
$data = $response->json(true, false);
// Or use the get method with a default
$value = $response->get('key', 'default value');
Handling Validation Errors
Many APIs return validation errors with status 422 (Unprocessable Entity):
$response = post('https://api.example.com/users', [
'email' => 'invalid-email',
'password' => '123' // Too short
]);
if ($response->isUnprocessableEntity()) {
$errors = $response->json()['errors'] ?? [];
foreach ($errors as $field => $messages) {
echo "- {$field}: " . implode(', ', $messages) . "\n";
}
}
Common API Error Formats
Different APIs structure their error responses differently. Here's how to handle some common formats:
Standard JSON API Errors
if ($response->failed()) {
$errorData = $response->json();
// Format: { "error": { "code": "invalid_token", "message": "The token is invalid" } }
if (isset($errorData['error']['message'])) {
echo "Error: " . $errorData['error']['message'];
echo "Code: " . $errorData['error']['code'] ?? 'unknown';
}
// Format: { "errors": [{ "title": "Invalid token", "detail": "The token is expired" }] }
elseif (isset($errorData['errors']) && is_array($errorData['errors'])) {
foreach ($errorData['errors'] as $error) {
echo $error['title'] . ": " . $error['detail'] . "\n";
}
}
// Format: { "message": "Validation failed", "errors": { "email": ["Invalid email"] } }
elseif (isset($errorData['message']) && isset($errorData['errors'])) {
echo $errorData['message'] . "\n";
foreach ($errorData['errors'] as $field => $messages) {
echo "- {$field}: " . implode(', ', $messages) . "\n";
}
}
// Simple format: { "message": "An error occurred" }
elseif (isset($errorData['message'])) {
echo "Error: " . $errorData['message'];
}
// Fallback
else {
echo "Unknown error occurred. Status code: " . $response->status();
}
}
Retry on Error
You can automatically retry requests that fail due to transient errors:
use Fetch\Http\ClientHandler;
$response = ClientHandler::create()
// Retry up to 3 times with exponential backoff
->retry(3, 100)
// Customize which status codes to retry
->retryStatusCodes([429, 503, 504])
// Customize which exceptions to retry
->retryExceptions([\GuzzleHttp\Exception\ConnectException::class])
->get('https://api.example.com/unstable-endpoint');
Handling Rate Limits
Many APIs implement rate limiting. Here's how to handle 429 Too Many Requests responses:
$response = get('https://api.example.com/users');
if ($response->isTooManyRequests()) {
// Check for Retry-After header (might be in seconds or a timestamp)
$retryAfter = $response->header('Retry-After');
if ($retryAfter !== null) {
if (is_numeric($retryAfter)) {
$waitSeconds = (int) $retryAfter;
} else {
// Parse HTTP date
$waitSeconds = strtotime($retryAfter) - time();
}
echo "Rate limited. Please try again after {$waitSeconds} seconds.";
// You could wait and retry automatically
if ($waitSeconds > 0 && $waitSeconds < 60) { // Only wait if reasonable
sleep($waitSeconds);
return get('https://api.example.com/users');
}
} else {
echo "Rate limited. Please try again later.";
}
}
Custom Error Handling Class
For more advanced applications, you might want to create a dedicated error handler:
class ApiErrorHandler
{
/**
* Handle a response that might contain errors.
*/
public function handleResponse($response)
{
if ($response->successful()) {
return $response;
}
switch ($response->status()) {
case 401:
throw new AuthenticationException("Authentication required");
case 403:
throw new AuthorizationException("You don't have permission to access this resource");
case 404:
throw new ResourceNotFoundException("The requested resource was not found");
case 422:
$errors = $response->json()['errors'] ?? [];
throw new ValidationException("Validation failed", $errors);
case 429:
$retryAfter = $response->header('Retry-After');
throw new RateLimitException("Too many requests", $retryAfter);
case 500:
case 502:
case 503:
case 504:
throw new ServerException("Server error: " . $response->status());
default:
throw new ApiException("API error: " . $response->status());
}
}
}
// Usage
$errorHandler = new ApiErrorHandler();
try {
$response = get('https://api.example.com/users');
$errorHandler->handleResponse($response);
// Process successful response
$users = $response->json();
} catch (AuthenticationException $e) {
// Handle authentication error
} catch (ValidationException $e) {
// Handle validation errors
$errors = $e->getErrors();
} catch (ApiException $e) {
// Handle other API errors
}
Debugging Errors
For debugging, you can use the debug()
method to get detailed information about a request:
try {
$client = fetch();
$debug = $client->debug();
// Attempt the request
$response = $client->get('https://api.example.com/users');
if ($response->failed()) {
echo "Request failed with status: " . $response->status() . "\n";
echo "Debug information:\n";
print_r($debug);
}
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . "\n";
echo "Debug information:\n";
print_r($debug);
}
Error Logging
You can use a PSR-3 compatible logger to log errors:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create a logger
$logger = new Logger('api');
$logger->pushHandler(new StreamHandler('logs/api.log', Logger::ERROR));
// Configure the client with the logger
fetch_client(logger: $logger);
// Now errors will be logged
try {
$response = get('https://api.example.com/users');
if ($response->failed()) {
// This will be logged by the client
throw new \Exception("API request failed: " . $response->status());
}
} catch (\Exception $e) {
// Additional custom logging if needed
$logger->error("Custom error handler: " . $e->getMessage());
}
Next Steps
- Learn about Retry Handling for automatic recovery from errors
- Explore Logging for more advanced error logging
- See Authentication for handling authentication errors