Table of contents

What are the best practices for error handling in Guzzle?

Effective error handling is essential when building robust PHP applications with Guzzle HTTP client. Poor error handling can lead to application crashes, poor user experience, and difficult debugging. This guide covers comprehensive best practices for handling errors in Guzzle applications.

Understanding Guzzle's Exception Hierarchy

Guzzle uses a well-structured exception hierarchy that allows for granular error handling:

use GuzzleHttp\Exception\RequestException;      // Base exception
use GuzzleHttp\Exception\ClientException;       // 4xx HTTP errors
use GuzzleHttp\Exception\ServerException;       // 5xx HTTP errors
use GuzzleHttp\Exception\ConnectException;      // Network connection errors
use GuzzleHttp\Exception\TooManyRedirectsException; // Redirect limit exceeded
use GuzzleHttp\Exception\BadResponseException;  // Base for HTTP error responses

Exception Types and When They Occur

  • RequestException: Base exception for all request-related errors
  • ClientException: 400-499 HTTP status codes (client errors like 404, 401)
  • ServerException: 500-599 HTTP status codes (server errors like 500, 503)
  • ConnectException: Network issues (DNS resolution, connection timeouts)
  • TooManyRedirectsException: Exceeding maximum redirect limit
  • TransferException: Data transfer problems

1. Implement Comprehensive Try-Catch Blocks

Use specific exception catching to handle different error types appropriately:

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;

$client = new Client();

try {
    $response = $client->request('GET', 'https://api.example.com/users/123');
    $data = json_decode($response->getBody()->getContents(), true);
    return $data;

} catch (ClientException $e) {
    // Handle 4xx errors (client-side issues)
    $statusCode = $e->getResponse()->getStatusCode();

    if ($statusCode === 404) {
        throw new UserNotFoundException('User not found');
    } elseif ($statusCode === 401) {
        throw new UnauthorizedException('Invalid authentication');
    } else {
        throw new ClientErrorException("Client error: {$statusCode}");
    }

} catch (ServerException $e) {
    // Handle 5xx errors (server-side issues)
    $statusCode = $e->getResponse()->getStatusCode();
    error_log("Server error {$statusCode}: " . $e->getMessage());
    throw new ServiceUnavailableException('External service temporarily unavailable');

} catch (ConnectException $e) {
    // Handle network connectivity issues
    error_log("Connection error: " . $e->getMessage());
    throw new NetworkException('Unable to connect to external service');

} catch (RequestException $e) {
    // Handle other request-related errors
    error_log("Request error: " . $e->getMessage());
    throw new ExternalServiceException('External service request failed');
}

2. Disable HTTP Error Exceptions When Needed

By default, Guzzle throws exceptions for 4xx and 5xx responses. You can disable this behavior:

try {
    $response = $client->request('GET', 'https://api.example.com/data', [
        'http_errors' => false // Disable automatic exception throwing
    ]);

    $statusCode = $response->getStatusCode();

    if ($statusCode >= 400) {
        $errorBody = $response->getBody()->getContents();
        $errorData = json_decode($errorBody, true);

        // Handle error based on status code and response content
        switch ($statusCode) {
            case 400:
                throw new ValidationException($errorData['message'] ?? 'Bad request');
            case 401:
                throw new AuthenticationException('Unauthorized access');
            case 429:
                throw new RateLimitException('Rate limit exceeded');
            default:
                throw new ApiException("API error: {$statusCode}");
        }
    }

    return json_decode($response->getBody()->getContents(), true);

} catch (RequestException $e) {
    // Handle network and other non-HTTP errors
    throw new NetworkException('Request failed: ' . $e->getMessage());
}

3. Implement Robust Retry Mechanisms

Use Guzzle's retry middleware for handling transient failures:

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;

// Create custom retry decider
$retryDecider = function (
    int $retries,
    Request $request,
    Response $response = null,
    RequestException $exception = null
) {
    // Limit the number of retries
    if ($retries >= 3) {
        return false;
    }

    // Retry on connection exceptions
    if ($exception instanceof ConnectException) {
        return true;
    }

    if ($response) {
        // Retry on server errors and rate limiting
        $statusCode = $response->getStatusCode();
        return in_array($statusCode, [500, 502, 503, 504, 429]);
    }

    return false;
};

// Create custom delay function (exponential backoff)
$retryDelay = function (int $numberOfRetries, Response $response = null) {
    // Check for Retry-After header
    if ($response && $response->hasHeader('Retry-After')) {
        $retryAfter = $response->getHeaderLine('Retry-After');
        return is_numeric($retryAfter) ? (int)$retryAfter * 1000 : 1000;
    }

    // Exponential backoff: 1s, 2s, 4s
    return (int) pow(2, $numberOfRetries) * 1000;
};

// Set up the handler stack
$handlerStack = HandlerStack::create();
$handlerStack->push(Middleware::retry($retryDecider, $retryDelay));

$client = new Client([
    'handler' => $handlerStack,
    'timeout' => 10,
    'connect_timeout' => 5,
]);

4. Configure Appropriate Timeouts

Always set timeouts to prevent hanging requests:

$client = new Client([
    'timeout' => 30.0,          // Total request timeout
    'connect_timeout' => 5.0,   // Connection establishment timeout
    'read_timeout' => 25.0,     // Time to wait for response
]);

// Per-request timeout override
$response = $client->request('GET', 'https://slow-api.example.com/data', [
    'timeout' => 60.0, // Override for this specific request
]);

5. Implement Comprehensive Logging

Create detailed logs for debugging and monitoring:

use Psr\Log\LoggerInterface;

class ApiClient
{
    private Client $client;
    private LoggerInterface $logger;

    public function __construct(Client $client, LoggerInterface $logger)
    {
        $this->client = $client;
        $this->logger = $logger;
    }

    public function makeRequest(string $method, string $url, array $options = [])
    {
        $startTime = microtime(true);

        try {
            $this->logger->info('Making HTTP request', [
                'method' => $method,
                'url' => $url,
                'options' => $this->sanitizeOptions($options)
            ]);

            $response = $this->client->request($method, $url, $options);

            $duration = microtime(true) - $startTime;

            $this->logger->info('HTTP request successful', [
                'method' => $method,
                'url' => $url,
                'status_code' => $response->getStatusCode(),
                'duration_ms' => round($duration * 1000, 2)
            ]);

            return $response;

        } catch (RequestException $e) {
            $duration = microtime(true) - $startTime;

            $context = [
                'method' => $method,
                'url' => $url,
                'duration_ms' => round($duration * 1000, 2),
                'error_message' => $e->getMessage()
            ];

            if ($e->hasResponse()) {
                $response = $e->getResponse();
                $context['status_code'] = $response->getStatusCode();
                $context['response_body'] = $response->getBody()->getContents();
            }

            $this->logger->error('HTTP request failed', $context);
            throw $e;
        }
    }

    private function sanitizeOptions(array $options): array
    {
        // Remove sensitive data from logging
        if (isset($options['headers']['Authorization'])) {
            $options['headers']['Authorization'] = '[REDACTED]';
        }
        return $options;
    }
}

6. Create Custom Error Handling Middleware

Build middleware for centralized error handling:

use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\FulfilledPromise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

function createErrorHandlingMiddleware(LoggerInterface $logger = null)
{
    return function (callable $handler) use ($logger) {
        return function (RequestInterface $request, array $options) use ($handler, $logger) {
            return $handler($request, $options)->then(
                function (ResponseInterface $response) use ($request, $logger) {
                    // Log successful responses if needed
                    return $response;
                },
                function ($reason) use ($request, $logger) {
                    if ($reason instanceof RequestException) {
                        // Custom error handling logic
                        $url = (string) $request->getUri();

                        if ($logger) {
                            $logger->warning('HTTP request failed', [
                                'url' => $url,
                                'method' => $request->getMethod(),
                                'error' => $reason->getMessage()
                            ]);
                        }

                        // Transform specific errors
                        if ($reason instanceof ConnectException) {
                            return new RejectedPromise(
                                new NetworkException('Network connectivity issue', 0, $reason)
                            );
                        }
                    }

                    return new RejectedPromise($reason);
                }
            );
        };
    };
}

// Apply the middleware
$stack = HandlerStack::create();
$stack->push(createErrorHandlingMiddleware($logger));

$client = new Client(['handler' => $stack]);

7. Implement Circuit Breaker Pattern

Prevent cascading failures with a circuit breaker:

class CircuitBreaker
{
    private int $failureCount = 0;
    private int $failureThreshold;
    private int $timeout;
    private ?int $lastFailureTime = null;
    private string $state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN

    public function __construct(int $failureThreshold = 5, int $timeout = 60)
    {
        $this->failureThreshold = $failureThreshold;
        $this->timeout = $timeout;
    }

    public function call(callable $callback)
    {
        if ($this->state === 'OPEN') {
            if (time() - $this->lastFailureTime < $this->timeout) {
                throw new CircuitBreakerOpenException('Circuit breaker is open');
            }
            $this->state = 'HALF_OPEN';
        }

        try {
            $result = $callback();
            $this->onSuccess();
            return $result;
        } catch (Exception $e) {
            $this->onFailure();
            throw $e;
        }
    }

    private function onSuccess(): void
    {
        $this->failureCount = 0;
        $this->state = 'CLOSED';
    }

    private function onFailure(): void
    {
        $this->failureCount++;
        $this->lastFailureTime = time();

        if ($this->failureCount >= $this->failureThreshold) {
            $this->state = 'OPEN';
        }
    }
}

// Usage with Guzzle
$circuitBreaker = new CircuitBreaker();

try {
    $response = $circuitBreaker->call(function () use ($client) {
        return $client->request('GET', 'https://api.example.com/data');
    });
} catch (CircuitBreakerOpenException $e) {
    // Use fallback or cached data
    return $this->getFallbackData();
}

8. Handle Specific Scenarios

Rate Limiting

try {
    $response = $client->request('GET', 'https://api.example.com/data');
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() === 429) {
        $retryAfter = $e->getResponse()->getHeaderLine('Retry-After');
        throw new RateLimitException("Rate limited. Retry after: {$retryAfter} seconds");
    }
    throw $e;
}

Authentication Errors

try {
    $response = $client->request('GET', 'https://api.example.com/protected', [
        'headers' => ['Authorization' => 'Bearer ' . $token]
    ]);
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() === 401) {
        // Attempt token refresh
        $newToken = $this->refreshToken();

        // Retry with new token
        return $client->request('GET', 'https://api.example.com/protected', [
            'headers' => ['Authorization' => 'Bearer ' . $newToken]
        ]);
    }
}

9. Testing Error Handling

Create comprehensive tests for your error handling:

use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;

class ApiClientTest extends TestCase
{
    public function testHandles404Error()
    {
        $mockResponse = new Response(404, [], '{"error": "Not found"}');
        $mockException = new ClientException('Not found', 
            $this->createMock(RequestInterface::class), 
            $mockResponse
        );

        $mockClient = $this->createMock(Client::class);
        $mockClient->method('request')->willThrowException($mockException);

        $apiClient = new ApiClient($mockClient);

        $this->expectException(UserNotFoundException::class);
        $apiClient->getUser(123);
    }
}

Key Takeaways

  1. Use specific exception catching for different error types
  2. Implement retry mechanisms for transient failures
  3. Set appropriate timeouts to prevent hanging requests
  4. Log comprehensive error information for debugging
  5. Consider disabling http_errors when you need custom response handling
  6. Implement circuit breakers for external service resilience
  7. Handle authentication and rate limiting scenarios specifically
  8. Test your error handling thoroughly

By following these best practices, you'll build more resilient PHP applications that gracefully handle the inevitable network and service failures that occur in distributed systems.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon