Using HTTP Proxies in PHP: A Complete Code-First Guide for cURL, Guzzle, Symfony, and Laravel

Learn how to configure HTTP proxies in PHP using raw cURL, Guzzle, Symfony HTTP Client, and Laravel. Includes concurrent scraping, TLS/SSL handling, and production-ready code examples.

Using HTTP Proxies in PHP: A Complete Code-First Guide for cURL, Guzzle, Symfony, and Laravel

Why PHP Developers Need HTTP Proxies

If you're building scrapers, integrating third-party APIs with strict rate limits, or accessing geo-restricted content, you've likely hit IP blocks. HTTP proxies solve this by routing your requests through different IP addresses—making your traffic appear to originate from various locations rather than a single source.

PHP developers have several powerful tools for proxy-aware HTTP requests: native cURL functions, Guzzle HTTP client, Symfony HTTP Client, and Laravel's wrapper ecosystem. Each has distinct advantages depending on your use case.

This guide shows you exactly how to configure PHP proxy connections across all major HTTP clients, with production-ready code you can copy and run.

Raw cURL: The Foundation of PHP Proxy Requests

PHP's native cURL extension gives you fine-grained control over proxy configuration. Every modern PHP HTTP client ultimately wraps cURL, so understanding the raw approach helps you debug and optimize.

Basic cURL Proxy Configuration

The key cURL options for proxy support are:

  • CURLOPT_PROXY — the proxy hostname or IP
  • CURLOPT_PROXYPORT — the proxy port
  • CURLOPT_PROXYUSERPWD — username:password for authentication
  • CURLOPT_HTTPPROXYTUNNEL — enable tunneling (recommended for HTTPS targets)

Here's a complete, runnable example using ProxyHat residential proxies:

<?php

function fetchWithProxy(string $url, string $username, string $password): string
{
    $ch = curl_init();
    
    // Basic cURL options
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        
        // Proxy configuration
        CURLOPT_PROXY => 'gate.proxyhat.com',
        CURLOPT_PROXYPORT => 8080,
        CURLOPT_PROXYUSERPWD => "{$username}:{$password}",
        CURLOPT_HTTPPROXYTUNNEL => true, // Required for HTTPS targets
        
        // SSL/TLS configuration
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt', // Adjust for your system
        
        // User agent (avoid detection)
        CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    ]);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    
    curl_close($ch);
    
    if ($error) {
        throw new RuntimeException("cURL error: {$error}");
    }
    
    if ($httpCode >= 400) {
        throw new RuntimeException("HTTP error: {$httpCode}");
    }
    
    return $response;
}

// Usage example
try {
    $html = fetchWithProxy(
        'https://httpbin.org/ip',
        'user-country-US', // ProxyHat geo-targeting
        'your-password'
    );
    echo $html;
} catch (RuntimeException $e) {
    echo "Request failed: " . $e->getMessage();
}

Geo-Targeting with ProxyHat

ProxyHat supports country and city-level targeting via the username field. Simply format your username as user-country-{CC} or user-country-{CC}-city-{city}:

<?php

// Target US IPs
$username = 'user-country-US';

// Target Berlin, Germany
$username = 'user-country-DE-city-berlin';

// Sticky session (same IP for multiple requests)
$username = 'user-session-abc123-country-US';

// Use in cURL
curl_setopt($ch, CURLOPT_PROXYUSERPWD, "{$username}:{$password}");

Guzzle HTTP Client: Modern PHP Proxy Integration

Guzzle is the most popular PHP HTTP client, offering a clean object-oriented interface. It's widely used in Laravel applications and standalone PHP projects.

Basic Guzzle Proxy Configuration

Pass proxy settings via the proxy request option:

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

function createGuzzleClient(string $username, string $password): Client
{
    return new Client([
        'timeout' => 30,
        'connect_timeout' => 10,
        'proxy' => "http://{$username}:{$password}@gate.proxyhat.com:8080",
        'headers' => [
            'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        ],
        'verify' => true, // Enable SSL verification
    ]);
}

// Usage
$client = createGuzzleClient('user-country-US', 'your-password');

try {
    $response = $client->get('https://httpbin.org/ip');
    echo $response->getBody()->getContents();
} catch (RequestException $e) {
    echo "Request failed: " . $e->getMessage();
}

Per-Request Proxy Rotation with Guzzle

For Guzzle proxy rotation, override the proxy setting on individual requests. This is essential for scraping where you need a different IP for each request:

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Promise;

class ProxyRotator
{
    private array $proxyCredentials = [];
    private int $currentIndex = 0;
    
    public function __construct(array $credentials)
    {
        $this->proxyCredentials = $credentials;
    }
    
    public function getNextProxyUrl(): string
    {
        $cred = $this->proxyCredentials[$this->currentIndex];
        $this->currentIndex = ($this->currentIndex + 1) % count($this->proxyCredentials);
        
        return "http://{$cred['username']}:{$cred['password']}@gate.proxyhat.com:8080";
    }
    
    public function getRotatingUsername(string $country = 'US'): string
    {
        // ProxyHat supports session-based rotation
        $sessionId = bin2hex(random_bytes(8));
        return "user-session-{$sessionId}-country-{$country}";
    }
}

// Production-ready scraping function
function scrapeMultipleUrls(array $urls, string $password): array
{
    $rotator = new ProxyRotator([]); // We'll use session-based rotation
    $client = new Client(['timeout' => 30, 'verify' => true]);
    
    $promises = [];
    
    foreach ($urls as $key => $url) {
        $username = $rotator->getRotatingUsername('US');
        $proxyUrl = "http://{$username}:{$password}@gate.proxyhat.com:8080";
        
        $promises[$key] = $client->getAsync($url, [
            'proxy' => $proxyUrl,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept' => 'text/html,application/xhtml+xml',
            ],
        ]);
    }
    
    // Wait for all requests to complete
    $results = Promise\Utils::settle($promises)->wait();
    
    $responses = [];
    foreach ($results as $key => $result) {
        if ($result['state'] === 'fulfilled') {
            $responses[$key] = $result['value']->getBody()->getContents();
        } else {
            $responses[$key] = ['error' => $result['reason']->getMessage()];
        }
    }
    
    return $responses;
}

// Usage
$urls = [
    'page1' => 'https://example.com/page1',
    'page2' => 'https://example.com/page2',
    'page3' => 'https://example.com/page3',
];

$results = scrapeMultipleUrls($urls, 'your-password');
print_r($results);

Symfony HTTP Client: Async and Modern

Symfony's HTTP Client offers excellent async support and integrates well with Laravel applications. It's particularly useful for high-throughput scraping scenarios.

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;

class SymfonyProxyClient
{
    private string $password;
    private $client;
    
    public function __construct(string $password)
    {
        $this->password = $password;
        $this->client = HttpClient::create([
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_peer' => true,
            'verify_host' => true,
        ]);
    }
    
    public function fetch(string $url, ?string $country = null): string
    {
        $username = $this->buildUsername($country);
        
        $response = $this->client->request('GET', $url, [
            'proxy' => "http://{$username}:{$this->password}@gate.proxyhat.com:8080",
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            ],
        ]);
        
        //getStatusCode() will throw on network errors
        $statusCode = $response->getStatusCode();
        
        if ($statusCode >= 400) {
            throw new RuntimeException("HTTP error: {$statusCode}");
        }
        
        return $response->getContent();
    }
    
    public function fetchConcurrent(array $urls, ?string $country = null): array
    {
        $responses = [];
        
        foreach ($urls as $key => $url) {
            $username = $this->buildUsername($country);
            
            $responses[$key] = $this->client->request('GET', $url, [
                'proxy' => "http://{$username}:{$this->password}@gate.proxyhat.com:8080",
                'headers' => [
                    'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                ],
            ]);
        }
        
        // Process responses as they complete
        $results = [];
        foreach ($responses as $key => $response) {
            try {
                $results[$key] = $response->getContent();
            } catch (\Exception $e) {
                $results[$key] = ['error' => $e->getMessage()];
            }
        }
        
        return $results;
    }
    
    private function buildUsername(?string $country): string
    {
        $sessionId = bin2hex(random_bytes(8));
        $country = $country ?? 'US';
        
        return "user-session-{$sessionId}-country-{$country}";
    }
}

// Usage
$client = new SymfonyProxyClient('your-password');

// Single request
$html = $client->fetch('https://httpbin.org/ip', 'US');
echo $html;

// Concurrent requests
$urls = [
    'ip1' => 'https://httpbin.org/ip',
    'ip2' => 'https://httpbin.org/user-agent',
];

$results = $client->fetchConcurrent($urls, 'DE');
print_r($results);

Laravel Integration: Production-Ready Proxy Service

For Laravel proxy scraping applications, you'll want a reusable service class that handles proxy rotation, retries, and logging. Here's a complete implementation:

Proxy Service Class

<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;

class ProxyService
{
    private Client $client;
    private string $proxyPassword;
    private int $maxRetries;
    private int $retryDelay;
    
    public function __construct(
        ?string $password = null,
        int $maxRetries = 3,
        int $retryDelay = 1000
    ) {
        $this->proxyPassword = $password ?? config('services.proxyhat.password');
        $this->maxRetries = $maxRetries;
        $this->retryDelay = $retryDelay;
        
        $this->client = new Client([
            'timeout' => config('services.proxyhat.timeout', 30),
            'connect_timeout' => config('services.proxyhat.connect_timeout', 10),
            'verify' => true,
        ]);
    }
    
    /**
     * Fetch URL with automatic proxy rotation and retries.
     */
    public function fetch(
        string $url,
        ?string $country = null,
        ?string $sessionId = null,
        array $headers = []
    ): string {\n        $attempt = 0;
        $lastException = null;
        
        while ($attempt < $this->maxRetries) {
            $attempt++;
            
            try {
                $username = $this->buildUsername($country, $sessionId);
                $proxyUrl = $this->buildProxyUrl($username);
                
                $response = $this->client->get($url, [
                    'proxy' => $proxyUrl,
                    'headers' => array_merge($this->getDefaultHeaders(), $headers),
                ]);
                
                Log::info('Proxy request successful', [
                    'url' => $url,
                    'country' => $country,
                    'attempt' => $attempt,
                ]);
                
                return $response->getBody()->getContents();
                
            } catch (RequestException $e) {
                $lastException = $e;
                
                Log::warning('Proxy request failed', [
                    'url' => $url,
                    'attempt' => $attempt,
                    'error' => $e->getMessage(),
                    'status_code' => $e->getResponse()?->getStatusCode(),
                ]);
                
                // Force new session on retry (new IP)
                $sessionId = null;
                
                if ($attempt < $this->maxRetries) {
                    usleep($this->retryDelay * 1000 * $attempt); // Exponential backoff
                }
            }
        }
        
        throw $lastException ?? new \RuntimeException('Unknown error');
    }
    
    /**
     * Fetch multiple URLs concurrently.
     */
    public function fetchBatch(array $urls, ?string $country = null): array
    {
        $promises = [];
        
        foreach ($urls as $key => $url) {
            $username = $this->buildUsername($country);
            $proxyUrl = $this->buildProxyUrl($username);
            
            $promises[$key] = $this->client->getAsync($url, [
                'proxy' => $proxyUrl,
                'headers' => $this->getDefaultHeaders(),
            ]);
        }
        
        $results = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
        
        $responses = [];
        foreach ($results as $key => $result) {
            if ($result['state'] === 'fulfilled') {
                $responses[$key] = $result['value']->getBody()->getContents();
            } else {
                $responses[$key] = ['error' => $result['reason']->getMessage()];
            }
        }
        
        return $responses;
    }
    
    /**
     * Create a sticky session (same IP for multiple requests).
     */
    public function createStickySession(string $country = 'US'): string
    {
        return bin2hex(random_bytes(16));
    }
    
    private function buildUsername(?string $country, ?string $sessionId = null): string
    {
        $country = $country ?? config('services.proxyhat.default_country', 'US');
        $sessionId = $sessionId ?? bin2hex(random_bytes(8));
        
        return "user-session-{$sessionId}-country-{$country}";
    }
    
    private function buildProxyUrl(string $username): string
    {
        return "http://{$username}:{$this->proxyPassword}@gate.proxyhat.com:8080";
    }
    
    private function getDefaultHeaders(): array
    {
        $userAgents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
        ];
        
        return [
            'User-Agent' => $userAgents[array_rand($userAgents)],
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language' => 'en-US,en;q=0.9',
            'Accept-Encoding' => 'gzip, deflate',
        ];
    }
}

Laravel Configuration

Add these settings to config/services.php:

<?php

// config/services.php
return [
    'proxyhat' => [
        'password' => env('PROXYHAT_PASSWORD'),
        'timeout' => env('PROXYHAT_TIMEOUT', 30),
        'connect_timeout' => env('PROXYHAT_CONNECT_TIMEOUT', 10),
        'default_country' => env('PROXYHAT_DEFAULT_COUNTRY', 'US'),
    ],
];

Usage in Laravel Jobs

<?php

namespace App\Jobs;

use App\Services\ProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class ScrapeProductJob implements ShouldQueue
{
    use InteractsWithQueue, Queueable;
    
    public function __construct(
        private string $url,
        private string $country = 'US'
    ) {}
    
    public function handle(ProxyService $proxyService): void
    {
        // Create sticky session for this job (same IP throughout)
        $sessionId = $proxyService->createStickySession($this->country);
        
        try {
            $html = $proxyService->fetch(
                $this->url,
                $this->country,
                $sessionId
            );
            
            // Parse and store data
            $data = $this->parseHtml($html);
            
            // If you need to make follow-up requests,
            // pass the same $sessionId to maintain the same IP
            
        } catch (\Exception $e) {
            $this->fail($e);
        }
    }
    
    private function parseHtml(string $html): array
    {
        // Your parsing logic here
        return [];
    }
}

// Dispatch
ScrapeProductJob::dispatch('https://example.com/product/123', 'DE');

Multi-cURL for High-Concurrency Scraping

For maximum performance, PHP's curl_multi_* functions let you execute hundreds of concurrent requests. This approach bypasses HTTP client overhead and gives you raw control:

<?php

class MultiCurlProxyScraper
{
    private string $proxyPassword;
    private int $concurrency;
    
    public function __construct(string $password, int $concurrency = 50)
    {
        $this->proxyPassword = $password;
        $this->concurrency = $concurrency;
    }
    
    public function fetchAll(array $urls, ?string $country = null): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];
        
        // Initialize all cURL handles
        foreach ($urls as $key => $url) {
            $sessionId = bin2hex(random_bytes(8));
            $username = "user-session-{$sessionId}-country-{$country ?? 'US'}";
            
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $url,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 5,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_PROXY => 'gate.proxyhat.com',
                CURLOPT_PROXYPORT => 8080,
                CURLOPT_PROXYUSERPWD => "{$username}:{$this->proxyPassword}",
                CURLOPT_HTTPPROXYTUNNEL => true,
                CURLOPT_SSL_VERIFYPEER => true,
                CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            ]);
            
            curl_multi_add_handle($mh, $ch);
            $handles[$key] = $ch;
        }
        
        // Execute all requests
        $active = null;
        do {
            $status = curl_multi_exec($mh, $active);
            
            if ($active) {
                curl_multi_select($mh); // Wait for activity
            }
        } while ($active && $status === CURLM_OK);
        
        // Collect results
        foreach ($handles as $key => $ch) {
            $response = curl_multi_getcontent($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            
            if ($error) {
                $results[$key] = ['error' => $error];
            } elseif ($httpCode >= 400) {
                $results[$key] = ['error' => "HTTP {$httpCode}"];
            } else {
                $results[$key] = $response;
            }
            
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }
        
        curl_multi_close($mh);
        
        return $results;
    }
}

// Usage - fetch 100 URLs concurrently
$scraper = new MultiCurlProxyScraper('your-password', concurrency: 50);

$urls = [];
for ($i = 0; $i < 100; $i++) {
    $urls["page{$i}"] = "https://httpbin.org/delay/1?n={$i}";
}

$start = microtime(true);
$results = $scraper->fetchAll($urls, 'US');
$elapsed = microtime(true) - $start;

echo "Fetched " . count($urls) . " URLs in " . round($elapsed, 2) . " seconds\n";
echo "Success rate: " . count(array_filter($results, 'is_string')) . "/" . count($urls) . "\n";

TLS/SSL Configuration and CA Bundle Handling

When proxying HTTPS traffic, proper certificate verification is critical. Here's how to handle TLS/SSL correctly:

Common SSL Issues

  • Missing CA bundle — PHP can't verify SSL certificates
  • Outdated CA bundle — Newer certificates aren't recognized
  • Self-signed certificates — Internal APIs may use custom CAs
  • Proxy MITM — Some corporate proxies intercept SSL

Production SSL Configuration

<?php

class SecureProxyClient
{
    private string $caBundlePath;
    private string $proxyPassword;
    
    public function __construct(string $password)
    {
        $this->proxyPassword = $password;
        $this->caBundlePath = $this->findCaBundle();
    }
    
    /**
     * Locate CA bundle on various systems.
     */
    private function findCaBundle(): string
    {
        // Common CA bundle locations
        $candidates = [
            // Linux (Debian/Ubuntu)
            '/etc/ssl/certs/ca-certificates.crt',
            // Linux (RHEL/CentOS)
            '/etc/pki/tls/certs/ca-bundle.crt',
            // Linux (Alpine)
            '/etc/ssl/certs/ca-certificates.crt',
            // macOS (Homebrew OpenSSL)
            '/usr/local/etc/openssl/cert.pem',
            // macOS (system)
            '/etc/ssl/cert.pem',
            // Windows (XAMPP)
            'C:\xampp\php\extras\ssl\cacert.pem',
            // Composer CA bundle (if installed)
            dirname(__DIR__) . '/vendor/cacert/cacert.pem',
        ];
        
        foreach ($candidates as $path) {
            if (file_exists($path)) {
                return $path;
            }
        }
        
        // Fallback: download Mozilla CA bundle
        $bundlePath = sys_get_temp_dir() . '/cacert.pem';
        
        if (!file_exists($bundlePath)) {
            $bundle = file_get_contents('https://curl.se/ca/cacert.pem');
            file_put_contents($bundlePath, $bundle);
        }
        
        return $bundlePath;
    }
    
    /**
     * Create a cURL handle with proper SSL configuration.
     */
    public function createSecureCurlHandle(string $url, string $username): \CurlHandle
    {
        $ch = curl_init();
        
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 5,
            CURLOPT_TIMEOUT => 30,
            
            // Proxy
            CURLOPT_PROXY => 'gate.proxyhat.com',
            CURLOPT_PROXYPORT => 8080,
            CURLOPT_PROXYUSERPWD => "{$username}:{$this->proxyPassword}",
            CURLOPT_HTTPPROXYTUNNEL => true,
            
            // SSL/TLS - Always verify in production
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2, // Strict hostname verification
            CURLOPT_CAINFO => $this->caBundlePath,
            
            // TLS version (minimum TLS 1.2)
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
            
            // Cipher configuration
            CURLOPT_SSL_CIPHER_LIST => 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256',
            
            // Enable certificate status checking (OCSP stapling)
            CURLOPT_CERTINFO => true,
        ]);
        
        return $ch;
    }
    
    /**
     * Guzzle client with SSL hardening.
     */
    public function createSecureGuzzleClient(): \GuzzleHttp\Client
    {
        return new \GuzzleHttp\Client([
            'timeout' => 30,
            'connect_timeout' => 10,
            'proxy' => "http://user-country-US:{$this->proxyPassword}@gate.proxyhat.com:8080",
            'verify' => $this->caBundlePath,
            'version' => 2.0, // HTTP/2 if available
            'config' => [
                'curl' => [
                    CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
                    CURLOPT_CERTINFO => true,
                ],
            ],
        ]);
    }
    
    /**
     * Debug SSL certificate chain.
     */
    public function inspectCertificate(string $url): array
    {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CERTINFO => true,
            CURLOPT_VERBOSE => true,
            CURLOPT_CAINFO => $this->caBundlePath,
        ]);
        
        curl_exec($ch);
        
        $certInfo = curl_getinfo($ch, CURLINFO_CERTINFO);
        $sslVerifyResult = curl_getinfo($ch, CURLINFO_SSL_VERIFYRESULT);
        
        curl_close($ch);
        
        return [
            'cert_info' => $certInfo,
            'ssl_verify_result' => $sslVerifyResult,
        ];
    }
}

// Usage
$client = new SecureProxyClient('your-password');

// Inspect SSL certificate
$certInfo = $client->inspectCertificate('https://example.com');
print_r($certInfo);

Handling SSL Errors

<?php

try {
    $response = $client->get('https://example.com');
} catch (GuzzleHttp\Exception\RequestException $e) {
    $curlError = curl_errno($e->getHandlerContext());
    
    // Common SSL error codes
    $sslErrors = [
        CURLE_SSL_CACERT => 'CA certificate not found',
        CURLE_SSL_CERTPROBLEM => 'Certificate problem',
        CURLE_SSL_CIPHER => 'Cipher negotiation failed',
        CURLE_SSL_CACERT_SHA256 => 'SHA-256 certificate issue',
        CURLE_SSL_PINNEDPUBKEYNOTMATCH => 'Public key mismatch',
        60 => 'Peer certificate cannot be authenticated',
        51 => 'SSL certificate verification failed',
    ];
    
    if (isset($sslErrors[$curlError])) {
        // Handle SSL-specific errors
        Log::error('SSL verification failed', [
            'error_code' => $curlError,
            'error_message' => $sslErrors[$curlError],
            'url' => $url,
        ]);
        
        // Options: update CA bundle, check for MITM, or investigate cert chain
    }
}

Comparison: PHP HTTP Clients with Proxy Support

FeatureRaw cURLGuzzleSymfony HTTPLaravel HTTP
Proxy ConfigurationManual optionsRequest optionsRequest optionsMiddleware
Async Supportcurl_multiPromisesNative asyncAsync pool
RotationManualPer-requestPer-requestService class
Retry LogicManualMiddlewareRetryableJob retries
SSL HandlingFull controlConfig optionConfig optionInherited
Best ForMaximum controlGeneral useHigh concurrencyLaravel apps

Key Takeaways

  • Raw cURL gives you maximum control over proxy configuration—use it when you need fine-tuned SSL settings or are building custom scraping pipelines.
  • Guzzle is the best all-around choice for most PHP projects—its promise-based async support and middleware system handle proxy rotation elegantly.
  • Symfony HTTP Client excels at high-concurrency workloads with native async support—ideal for scraping thousands of pages.
  • Laravel integration should wrap proxy logic in a reusable service class that handles rotation, retries, and logging—inject it into jobs for queue-based scraping.
  • SSL verification must never be disabled in production—locate and configure a proper CA bundle, and handle certificate errors gracefully.
  • ProxyHat geo-targeting lets you specify country and city in the username string (user-country-US-city-newyork), and sticky sessions maintain the same IP across multiple requests.

Ready to start scraping? Get residential proxies from ProxyHat with instant access to millions of IPs across 195+ countries.

Ready to get started?

Access 50M+ residential IPs across 148+ countries with AI-powered filtering.

View PricingResidential Proxies
← Back to Blog