Guia Completo de Proxy HTTP em PHP: cURL, Guzzle e Laravel

Aprenda a configurar proxies HTTP em PHP usando cURL nativo, Guzzle, Symfony HTTP Client e Laravel. Inclui exemplos de código para rotação de IPs, requisições concorrentes e configuração TLS/SSL.

Guia Completo de Proxy HTTP em PHP: cURL, Guzzle e Laravel

Se você está construindo um scraper, integrando APIs de terceiros com rate limits agressivos, ou precisa acessar recursos geograficamente restritos, dominar proxies HTTP em PHP é uma habilidade essencial. Este guia mostra como configurar proxies em cada camada da stack PHP — desde cURL bruto até uma solução pronta para produção em Laravel.

Por que usar proxies em aplicações PHP?

Proxies HTTP resolvem três problemas principais: rate limiting (muitos sites bloqueiam após X requisições do mesmo IP), geo-blocking (conteúdo restrito por região), e anonimato (proteger sua infraestrutura de ser identificada). Para scraping e automação, proxies residenciais são particularmente valiosos porque parecem tráfego de usuários reais.

O ecossistema PHP oferece várias abordagens para trabalhar com proxies. Vamos explorar cada uma, começando pelo nível mais baixo.

1. cURL nativo: configurando CURLOPT_PROXY

O cURL é a fundação de praticamente todo cliente HTTP em PHP. Entender suas opções de proxy é fundamental. As duas constantes principais são CURLOPT_PROXY (endereço do servidor) e CURLOPT_PROXYUSERPWD (credenciais no formato usuario:senha).

<?php

/**
 * Exemplo básico de requisição com proxy usando cURL nativo
 */
function fetchWithProxy(string $url, string $proxyUrl, ?string $credentials = null): string
{
    $ch = curl_init();
    
    // URL de destino
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 5);
    
    // Configuração do proxy
    // Formato: gate.proxyhat.com:8080
    curl_setopt($ch, CURLOPT_PROXY, $proxyUrl);
    
    // Credenciais no formato usuario:senha
    if ($credentials) {
        curl_setopt($ch, CURLOPT_PROXYUSERPWD, $credentials);
    }
    
    // Timeout para evitar travamentos
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
    
    // TLS/SSL - verificar certificado
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
    
    // Executar requisição
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    
    curl_close($ch);
    
    if ($error) {
        throw new RuntimeException("Erro cURL: {$error}");
    }
    
    if ($httpCode >= 400) {
        throw new RuntimeException("HTTP {$httpCode} recebido de {$url}");
    }
    
    return $response;
}

// Uso com ProxyHat
$proxyHost = 'gate.proxyhat.com:8080';
$credentials = 'usuario-proxy:senha-proxy';

try {
    $html = fetchWithProxy('https://httpbin.org/ip', $proxyHost, $credentials);
    echo $html;
} catch (RuntimeException $e) {
    echo "Falha: {$e->getMessage()}\n";
}

Para proxies SOCKS5, adicione curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPRO_SOCKS5); e use a porta 1080.

Rotação de IP por requisição

Com proxies residenciais, cada requisição pode sair de um IP diferente. O ProxyHat permite controlar isso via flags no username:

<?php

/**
 * Gera credenciais com rotação automática ou sessão fixa
 */
function buildProxyCredentials(
    string $username,
    string $password,
    ?string $country = null,
    ?string $session = null
): string {
    $user = $username;
    
    // Adiciona país para geo-targeting
    if ($country) {
        $user .= "-country-{$country}";
    }
    
    // Sessão fixa mantém o mesmo IP entre requisições
    if ($session) {
        $user .= "-session-{$session}";
    }
    
    return "{$user}:{$password}";
}

// Exemplo: IP diferente para cada requisição (rotação automática)
$rotating = buildProxyCredentials('meu_user', 'minha_senha', 'US', null);

// Exemplo: Mesmo IP para múltiplas requisições (sticky session)
$sticky = buildProxyCredentials('meu_user', 'minha_senha', 'US', 'sessao123');

2. Guzzle: proxy via request options

O Guzzle é o cliente HTTP mais popular em PHP. Ele abstrai o cURL mas expõe todas as opções relevantes. A configuração de proxy é passada no array de opções.

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class ProxyClient
{
    private Client $client;
    private array $proxyConfig;
    
    public function __construct(string $proxyHost, string $username, string $password)
    {
        $this->proxyConfig = [
            'proxy' => [
                'http' => "http://{$username}:{$password}@{$proxyHost}",
                'https' => "http://{$username}:{$password}@{$proxyHost}",
            ],
            'timeout' => 30,
            'connect_timeout' => 10,
            'verify' => true, // Verificar certificados SSL
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            ],
        ];
        
        $this->client = new Client($this->proxyConfig);
    }
    
    /**
     * Faz requisição com retry automático em caso de falha
     */
    public function fetch(string $url, int $maxRetries = 3): string
    {
        $lastException = null;
        
        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $response = $this->client->get($url);
                return (string) $response->getBody();
            } catch (RequestException $e) {
                $lastException = $e;
                
                // Backoff exponencial
                $delay = pow(2, $attempt) * 1000;
                usleep($delay * 1000);
                
                // Log do erro
                error_log("Tentativa {$attempt} falhou: {$e->getMessage()}");
            }
        }
        
        throw $lastException;
    }
    
    /**
     * Requisição com sessão fixa (mesmo IP)
     */
    public function fetchWithSession(string $url, string $sessionId, ?string $country = null): string
    {
        $username = 'meu_user';
        $password = 'minha_senha';
        
        if ($country) {
            $username .= "-country-{$country}";
        }
        $username .= "-session-{$sessionId}";
        
        $proxyUrl = "http://{$username}:{$password}@gate.proxyhat.com:8080";
        
        // Override da configuração de proxy para esta requisição específica
        $response = $this->client->get($url, [
            'proxy' => [
                'http' => $proxyUrl,
                'https' => $proxyUrl,
            ],
        ]);
        
        return (string) $response->getBody();
    }
}

// Uso
$client = new ProxyClient('gate.proxyhat.com:8080', 'meu_user', 'minha_senha');

try {
    $content = $client->fetch('https://httpbin.org/ip');
    echo $content;
    
    // Com sessão fixa
    $content = $client->fetchWithSession('https://httpbin.org/ip', 'sessao-abc123', 'US');
    echo $content;
} catch (Exception $e) {
    echo "Erro: {$e->getMessage()}\n";
}

Middleware de rotação automática

Para rotação automática de proxy por requisição, você pode criar um middleware Guzzle:

<?php

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;

class RotatingProxyMiddleware
{
    private array $proxyPool;
    private int $currentIndex = 0;
    
    public function __construct(array $proxies)
    {
        $this->proxyPool = $proxies;
    }
    
    public function __invoke(callable $handler)
    {
        return function (RequestInterface $request, array $options) use ($handler) {
            // Seleciona próximo proxy do pool (round-robin)
            $proxy = $this->proxyPool[$this->currentIndex];
            $this->currentIndex = ($this->currentIndex + 1) % count($this->proxyPool);
            
            // Adiciona proxy às opções
            $options['proxy'] = $proxy;
            
            return $handler($request, $options);
        };
    }
}

// Configuração
$proxyPool = [
    'http://user1:pass1@gate.proxyhat.com:8080',
    'http://user2:pass2@gate.proxyhat.com:8080',
    'http://user3:pass3@gate.proxyhat.com:8080',
];

$stack = HandlerStack::create();
$stack->push(new RotatingProxyMiddleware($proxyPool));

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

3. Symfony HTTP Client: async e proxy

O Symfony HTTP Client oferece suporte nativo a requisições assíncronas, ideal para scraping em larga escala. A configuração de proxy é simples e performática.

<?php

require 'vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;

class AsyncProxyClient
{
    private $client;
    private string $proxyUrl;
    
    public function __construct(string $username, string $password, string $proxyHost = 'gate.proxyhat.com:8080')
    {
        $this->proxyUrl = "http://{$username}:{$password}@{$proxyHost}";
        
        $this->client = HttpClient::create([
            'proxy' => $this->proxyUrl,
            'timeout' => 30,
            'max_redirects' => 5,
            'verify_peer' => true,
            'verify_host' => true,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (compatible; ProxyClient/1.0)',
            ],
        ]);
    }
    
    /**
     * Requisição síncrona simples
     */
    public function fetch(string $url): string
    {
        $response = $this->client->request('GET', $url);
        
        // Lança exceção em caso de erro HTTP
        if ($response->getStatusCode() >= 400) {
            throw new RuntimeException("HTTP {$response->getStatusCode()} de {$url}");
        }
        
        return $response->getContent();
    }
    
    /**
     * Múltiplas requisições concorrentes
     */
    public function fetchConcurrent(array $urls, int $concurrency = 10): array
    {
        $responses = [];
        $results = []
        ;
        
        // Iniciar todas as requisições
        foreach ($urls as $key => $url) {
            $responses[$key] = $this->client->request('GET', $url);
        }
        
        // Processar respostas conforme chegam
        foreach ($responses as $key => $response) {
            try {
                $results[$key] = [
                    'status' => $response->getStatusCode(),
                    'content' => $response->getContent(),
                    'headers' => $response->getHeaders(),
                ];
            } catch (TransportException $e) {
                $results[$key] = [
                    'error' => $e->getMessage(),
                    'status' => 0,
                ];
            }
        }
        
        return $results;
    }
    
    /**
     * Streaming de resposta grande
     */
    public function streamResponse(string $url, callable $onChunk): void
    {
        $response = $this->client->request('GET', $url);
        
        foreach ($this->client->stream($response) as $chunk) {
            if ($chunk->isFirst()) {
                // Headers recebidos
                echo "Status: {$response->getStatusCode()}\n";
            }
            
            if ($chunk->isLast()) {
                // Conexão fechada
                echo "Concluído\n";
                return;
            }
            
            // Processar chunk
            $onChunk($chunk->getContent());
        }
    }
}

// Uso
$client = new AsyncProxyClient('meu_user', 'minha_senha');

// Requisição única
$content = $client->fetch('https://httpbin.org/ip');

// Requisições concorrentes
$urls = [
    'page1' => 'https://httpbin.org/ip',
    'page2' => 'https://httpbin.org/headers',
    'page3' => 'https://httpbin.org/user-agent',
];

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

4. Integração Laravel: service class com pool de proxies

Em aplicações Laravel, o ideal é encapsular a lógica de proxy em uma service class injetável via container. Isso permite uso em controllers, jobs e queues de forma limpa e testável.

<?php

namespace App\Services;

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

class ResidentialProxyService
{
    private Client $client;
    private array $config;
    private ?string $currentSession = null;
    
    // Pool de países disponíveis
    private const SUPPORTED_COUNTRIES = ['US', 'BR', 'DE', 'GB', 'FR', 'ES', 'IT', 'JP'];
    
    public function __construct()
    {
        $this->config = [
            'host' => config('services.proxyhat.host', 'gate.proxyhat.com'),
            'port' => config('services.proxyhat.port', 8080),
            'username' => config('services.proxyhat.username'),
            'password' => config('services.proxyhat.password'),
        ];
        
        $this->client = new Client([
            'timeout' => 30,
            'connect_timeout' => 10,
            'verify' => true,
        ]);
    }
    
    /**
     * Faz requisição com IP rotativo (cada requisição = IP diferente)
     */
    public function fetchRotating(string $url, ?string $country = null): string
    {
        $proxyUrl = $this->buildProxyUrl(null, $country);
        return $this->executeWithRetry($url, $proxyUrl);
    }
    
    /**
     * Faz requisição com sessão fixa (mesmo IP mantido)
     */
    public function fetchSticky(string $url, string $sessionId, ?string $country = null): string
    {
        $proxyUrl = $this->buildProxyUrl($sessionId, $country);
        return $this->executeWithRetry($url, $proxyUrl);
    }
    
    /**
     * Inicia nova sessão e retorna ID
     */
    public function createSession(?string $country = null): string
    {
        $sessionId = 'sess_' . bin2hex(random_bytes(8));
        $this->currentSession = $sessionId;
        
        // Cache da sessão para reuso
        Cache::put("proxy_session:{$sessionId}", [
            'country' => $country,
            'created_at' => now(),
        ], now()->addMinutes(30));
        
        return $sessionId;
    }
    
    /**
     * Obtém sessão atual ou cria nova
     */
    public function getOrCreateSession(?string $country = null): string
    {
        if ($this->currentSession && Cache::has("proxy_session:{$this->currentSession}")) {
            return $this->currentSession;
        }
        
        return $this->createSession($country);
    }
    
    /**
     * Constrói URL do proxy com parâmetros opcionais
     */
    private function buildProxyUrl(?string $sessionId = null, ?string $country = null): string
    {
        $username = $this->config['username'];
        
        // Geo-targeting
        if ($country && in_array($country, self::SUPPORTED_COUNTRIES)) {
            $username .= "-country-{$country}";
        }
        
        // Sessão sticky
        if ($sessionId) {
            $username .= "-session-{$sessionId}";
        }
        
        return sprintf(
            'http://%s:%s@%s:%d',
            $username,
            $this->config['password'],
            $this->config['host'],
            $this->config['port']
        );
    }
    
    /**
     * Executa requisição com retry e logging
     */
    private function executeWithRetry(string $url, string $proxyUrl, int $maxRetries = 3): string
    {
        $lastException = null;
        
        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            try {
                $startTime = microtime(true);
                
                $response = $this->client->get($url, [
                    'proxy' => ['http' => $proxyUrl, 'https' => $proxyUrl],
                ]);
                
                $duration = round((microtime(true) - $startTime) * 1000);
                
                // Log de sucesso
                Log::channel('proxy')->info('Requisição bem-sucedida', [
                    'url' => $url,
                    'attempt' => $attempt,
                    'duration_ms' => $duration,
                    'status' => $response->getStatusCode(),
                ]);
                
                return (string) $response->getBody();
                
            } catch (RequestException $e) {
                $lastException = $e;
                
                Log::channel('proxy')->warning('Tentativa falhou', [
                    'url' => $url,
                    'attempt' => $attempt,
                    'error' => $e->getMessage(),
                    'status' => $e->getResponse()?->getStatusCode(),
                ]);
                
                // Backoff exponencial com jitter
                $delay = pow(2, $attempt) * 1000 + rand(0, 500);
                usleep($delay * 1000);
            }
        }
        
        Log::channel('proxy')->error('Todas as tentativas falharam', [
            'url' => $url,
            'attempts' => $maxRetries,
            'last_error' => $lastException?->getMessage(),
        ]);
        
        throw new RuntimeException(
            "Falha após {$maxRetries} tentativas: {$lastException?->getMessage()}"
        );
    }
    
    /**
     * Verifica saúde do proxy
     */
    public function healthCheck(): array
    {
        try {
            $response = $this->client->get('https://httpbin.org/ip', [
                'proxy' => ['http' => $this->buildProxyUrl(), 'https' => $this->buildProxyUrl()],
                'timeout' => 10,
            ]);
            
            return [
                'status' => 'healthy',
                'ip' => json_decode($response->getBody(), true)['origin'] ?? null,
                'latency_ms' => $response->getHeader('X-Response-Time')[0] ?? null,
            ];
        } catch (Exception $e) {
            return [
                'status' => 'unhealthy',
                'error' => $e->getMessage(),
            ];
        }
    }
}

Configuração e registro no container

Registre o serviço no AppServiceProvider e adicione as configurações:

<?php

// config/services.php
return [
    'proxyhat' => [
        'host' => env('PROXYHAT_HOST', 'gate.proxyhat.com'),
        'port' => env('PROXYHAT_PORT', 8080),
        'username' => env('PROXYHAT_USERNAME'),
        'password' => env('PROXYHAT_PASSWORD'),
    ],
];

// app/Providers/AppServiceProvider.php
public function register()
{
    $this->app->singleton(ResidentialProxyService::class, function ($app) {
        return new ResidentialProxyService();
    });
}

// Uso em um Job
namespace App\Jobs;

use App\Services\ResidentialProxyService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;

class ScrapeProductJob implements ShouldQueue
{
    use Queueable;
    
    public function __construct(private string $productUrl, private ?string $country = null) {}
    
    public function handle(ResidentialProxyService $proxy): void
    {
        $sessionId = $proxy->createSession($this->country);
        
        $html = $proxy->fetchSticky($this->productUrl, $sessionId);
        
        // Processar HTML...
        // $product = $this->parseProduct($html);
        // Product::create($product);
    }
}

5. Multi-cURL para scraping concorrente

Para máxima performance em scraping de múltiplas URLs simultaneamente, o curl_multi_* permite executar dezenas de requisições em paralelo com um único processo PHP.

<?php

class ConcurrentProxyScraper
{
    private string $proxyHost;
    private string $proxyAuth;
    private int $maxConcurrent;
    
    public function __construct(
        string $username,
        string $password,
        int $maxConcurrent = 20,
        string $host = 'gate.proxyhat.com:8080'
    ) {
        $this->proxyHost = $host;
        $this->proxyAuth = "{$username}:{$password}";
        $this->maxConcurrent = $maxConcurrent;
    }
    
    /**
     * Executa múltiplas requisições em paralelo
     */
    public function fetchBatch(array $urls): array
    {
        $results = [];
        $handles = [];
        
        // Inicializar multi-handle
        $mh = curl_multi_init();
        
        // Configurar timeout para multi-handle
        curl_multi_setopt($mh, CURLMOPT_MAX_TOTAL_CONNECTIONS, $this->maxConcurrent);
        
        // Criar handles individuais
        foreach ($urls as $key => $url) {
            $ch = $this->createHandle($url, $key);
            $handles[$key] = $ch;
            curl_multi_add_handle($mh, $ch);
        }
        
        // Executar todas as requisições
        $active = null;
        do {
            $status = curl_multi_exec($mh, $active);
            
            if ($status === CURLM_OK) {
                // Aguardar atividade
                curl_multi_select($mh, 1.0);
            }
        } while ($status === CURLM_OK && $active);
        
        // Coletar resultados
        foreach ($handles as $key => $ch) {
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            $content = curl_multi_getcontent($ch);
            
            $results[$key] = [
                'status' => $httpCode,
                'content' => $content,
                'error' => $error ?: null,
                'total_time' => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
                'size_download' => curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD),
            ];
            
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }
        
        curl_multi_close($mh);
        
        return $results;
    }
    
    /**
     * Cria handle cURL configurado
     */
    private function createHandle(string $url, string|int $key): CurlHandle
    {
        $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 => $this->proxyHost,
            CURLOPT_PROXYUSERPWD => $this->proxyAuth,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            // Identificador para debug
            CURLOPT_PRIVATE => $key,
        ]);
        
        return $ch;
    }
    
    /**
     * Processa URLs em chunks para controle de memória
     */
    public function fetchBatchChunked(array $urls, callable $onChunk, int $chunkSize = 50): void
    {
        $chunks = array_chunk($urls, $chunkSize, true);
        
        foreach ($chunks as $chunk) {
            $results = $this->fetchBatch($chunk);
            $onChunk($results);
            
            // Limpar memória entre chunks
            gc_collect_cycles();
        }
    }
}

// Uso
$scraper = new ConcurrentProxyScraper('meu_user', 'minha_senha', 20);

$urls = [
    'product_1' => 'https://exemplo.com/produto/1',
    'product_2' => 'https://exemplo.com/produto/2',
    'product_3' => 'https://exemplo.com/produto/3',
    // ... até centenas de URLs
];

// Processar em lotes
$scraper->fetchBatchChunked($urls, function (array $results) {
    foreach ($results as $key => $data) {
        if ($data['error']) {
            echo "Erro em {$key}: {$data['error']}\n";
            continue;
        }
        
        // Salvar no banco ou processar
        echo "{$key}: {$data['status']} - {$data['size_download']} bytes\n";
    }
}, 50);

6. TLS/SSL e CA bundle handling

Trabalhar com proxies requer atenção especial à verificação de certificados. Alguns proxies podem interceptar HTTPS (man-in-the-middle), o que exige configuração específica do CA bundle.

<?php

class TlsAwareProxyClient
{
    private string $proxyHost;
    private string $proxyAuth;
    private ?string $caBundlePath;
    
    public function __construct(
        string $username,
        string $password,
        string $host = 'gate.proxyhat.com:8080'
    ) {
        $this->proxyHost = $host;
        $this->proxyAuth = "{$username}:{$password}";
        $this->caBundlePath = $this->findCaBundle();
    }
    
    /**
     * Localiza CA bundle do sistema
     */
    private function findCaBundle(): ?string
    {
        // Caminhos comuns do CA bundle
        $candidates = [
            // Linux (Debian/Ubuntu)
            '/etc/ssl/certs/ca-certificates.crt',
            // Linux (RHEL/CentOS)
            '/etc/pki/tls/certs/ca-bundle.crt',
            // Linux (Arch)
            '/etc/ssl/certs/ca-bundle.crt',
            // macOS (Homebrew)
            '/usr/local/etc/openssl/cert.pem',
            // Windows (curl)
            'C:\\Windows\\System32\\curl-ca-bundle.crt',
            // Composer CA bundle (fallback)
            dirname(__DIR__) . '/vendor/cacert.pem',
        ];
        
        foreach ($candidates as $path) {
            if (file_exists($path) && is_readable($path)) {
                return $path;
            }
        }
        
        // Fallback: usar CA bundle do Guzzle
        $guzzleBundle = dirname(__DIR__) . '/vendor/guzzlehttp/guzzle/src/Handler/cacert.pem';
        if (file_exists($guzzleBundle)) {
            return $guzzleBundle;
        }
        
        return null;
    }
    
    /**
     * Requisição com verificação TLS estrita
     */
    public function fetchStrict(string $url): string
    {
        $ch = curl_init();
        
        $options = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYUSERPWD => $this->proxyAuth,
            CURLOPT_TIMEOUT => 30,
            
            // TLS estrito
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2,
            
            // CA bundle específico
            CURLOPT_CAINFO => $this->caBundlePath,
            
            // Verificar revogação de certificado
            CURLOPT_SSL_OPTIONS => CURLSSLOPT_REVOKE_BEST_EFFORT,
        ];
        
        curl_setopt_array($ch, $options);
        
        $response = curl_exec($ch);
        
        if (curl_errno($ch)) {
            $error = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException("Erro TLS: {$error}");
        }
        
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode >= 400) {
            throw new RuntimeException("HTTP {$httpCode}");
        }
        
        return $response;
    }
    
    /**
     * Requisição com TLS flexível (para proxies que interceptam)
     * ATENÇÃO: Menos seguro, use apenas em ambiente controlado
     */
    public function fetchFlexible(string $url): string
    {
        $ch = curl_init();
        
        $options = [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_PROXY => $this->proxyHost,
            CURLOPT_PROXYUSERPWD => $this->proxyAuth,
            CURLOPT_TIMEOUT => 30,
            
            // TLS flexível (não verificar certificado do proxy)
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
        ];
        
        // Aviso de segurança
        trigger_error(
            'fetchFlexible desabilita verificação TLS. Use apenas em desenvolvimento.',
            E_USER_NOTICE
        );
        
        curl_setopt_array($ch, $options);
        
        $response = curl_exec($ch);
        
        if (curl_errno($ch)) {
            throw new RuntimeException(curl_error($ch));
        }
        
        curl_close($ch);
        return $response;
    }
    
    /**
     * Diagnóstico de conectividade TLS
     */
    public function diagnoseTls(string $testUrl = 'https://httpbin.org/get'): array
    {
        $results = [];
        
        // Teste 1: Sem proxy (baseline)
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $testUrl,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_CAINFO => $this->caBundlePath,
        ]);
        
        $results['direct'] = [
            'success' => curl_exec($ch) !== false,
            'error' => curl_error($ch),
        ];
        curl_close($ch);
        
        // Teste 2: Com proxy, TLS estrito
        try {
            $results['proxy_strict'] = [
                'success' => true,
                'response' => $this->fetchStrict($testUrl),
            ];
        } catch (Exception $e) {
            $results['proxy_strict'] = [
                'success' => false,
                'error' => $e->getMessage(),
            ];
        }
        
        // Informações do CA bundle
        $results['ca_bundle'] = [
            'path' => $this->caBundlePath,
            'exists' => $this->caBundlePath && file_exists($this->caBundlePath),
        ];
        
        return $results;
    }
}

// Exemplo de uso com diagnóstico
$client = new TlsAwareProxyClient('meu_user', 'minha_senha');

// Diagnóstico antes de usar em produção
$diag = $client->diagnoseTls();
print_r($diag);

// Requisição segura
try {
    $content = $client->fetchStrict('https://api.exemplo.com/data');
    echo "Requisição bem-sucedida\n";
} catch (RuntimeException $e) {
    echo "Erro: {$e->getMessage()}\n";
}

Comparação: cURL vs Guzzle vs Symfony HTTP Client

Característica cURL nativo Guzzle Symfony HTTP Client
Curva de aprendizado Alta (API verbosa) Baixa (abstrações claras) Média (conceitos async)
Performance Máxima (direto) Boa (wrapper cURL) Excelente (async nativo)
Concorrência curl_multi (complexo) Pool (middleware) Async nativo (simples)
Integração Laravel Manual Excelente (package oficial) Boa (bridge disponível)
Testabilidade Difícil (mock complexo) Excelente (handlers mock) Boa (mock client)
Dependências Nenhuma (extensão) psr/http-message symfony/http-client

Boas práticas para produção

  • Timeouts adequados: Sempre configure CURLOPT_TIMEOUT e CURLOPT_CONNECTTIMEOUT. Valores recomendados: 30s para timeout total, 10s para conexão.
  • Retry com backoff: Implemente retry exponencial com jitter para evitar thundering herd em caso de falhas do proxy.
  • Logging estruturado: Registre URL, status, latência e IP de saída para auditoria e debugging.
  • Rate limiting próprio: Mesmo com proxies, respeite rate limits do target. Adicione delays entre requisições.
  • User-Agent realista: Use User-Agents de browsers reais para evitar detecção básica.
  • Rotação de sessões: Para scraping longo, alterne sessões periodicamente para evitar blocks por comportamento.
  • Monitoramento: Implemente health checks periódicos do proxy e alertas para degradação.

Nota sobre ética: Sempre verifique o robots.txt e os termos de serviço do site alvo. Use delays entre requisições e considere horários de menor tráfego. Para dados sensíveis, consulte um advogado sobre conformidade com LGPD/GDPR.

Key Takeaways

  • cURL nativo oferece controle total e máxima performance, mas exige mais código. Ideal para scripts simples ou quando você precisa de cada detalhe.
  • Guzzle é a escolha mais produtiva para aplicações Laravel, com middleware para rotação automática e excelente testabilidade.
  • Symfony HTTP Client brilha em cenários de alta concorrência com seu modelo async nativo e streaming.
  • Multi-cURL é a ferramenta certa quando você precisa de centenas de requisições paralelas em um único processo.
  • TLS/SSL requer atenção especial: sempre use verificação estrita em produção, e tenha um CA bundle atualizado.
  • Service classes em Laravel permitem encapsular toda a lógica de proxy, com injeção de dependência e uso transparente em jobs e controllers.

Para mais exemplos de uso e casos avançados, confira nossa documentação em /pt/use-cases/web-scraping e /pt/pricing para detalhes dos planos de proxies residenciais.

Pronto para começar?

Acesse mais de 50M de IPs residenciais em mais de 148 países com filtragem por IA.

Ver preçosProxies residenciais
← Voltar ao Blog