Geonode logo
Geonode Team

Geonode Team

Actualizado: 16 de octubre de 2026

Publicado: 16 de octubre de 2026

Configuración de Guzzle Proxy en PHP: autenticación, HTTPS y resolución de problemas

Esta guía explica paso a paso cada parte de una configuración funcional de un proxy con Guzzle: la opción de solicitud `proxy` en sus formatos de cadena y matriz, las URL de proxy autenticadas, los destinos HTTPS a través de un túnel CONNECT, los tiempos de espera y la verificación de certificados, así como las excepciones que lanza Guzzle cuando el proxy rechaza la solicitud. Al finalizar, dispondrás de un cliente PHP reutilizable que se conecta a través de un proxy residencial, mantiene una misma IP de salida durante toda la sesión, verifica dicha IP y vuelve a intentarlo con una nueva sesión cuando falla una solicitud. Todos los ejemplos están pensados para Guzzle 7 y PHP 8.1 o posterior. Instálalo con `composer require guzzlehttp/guzzle:^7`.

Esta guía explica paso a paso cada parte de una configuración funcional de un proxy con Guzzle: la opción de solicitud proxy en sus formatos de cadena y matriz, las URL de proxy autenticadas, los destinos HTTPS a través de un túnel CONNECT, los tiempos de espera y la verificación de certificados, así como las excepciones que lanza Guzzle cuando el proxy rechaza la solicitud. Al finalizar, dispondrás de un cliente PHP reutilizable que se conecta a través de un proxy residencial, mantiene una misma IP de salida durante toda la sesión, verifica dicha IP y vuelve a intentarlo con una nueva sesión cuando falla una solicitud.

Todos los ejemplos están pensados para Guzzle 7 y PHP 8.1 o posterior. Instálalo con composer require guzzlehttp/guzzle:^7.

La opción proxy: cadena o matriz

Guzzle configura un proxy mediante la opción de solicitud proxy, documentada en docs.guzzlephp.org. Se puede pasar por solicitud o en el constructor del cliente como valor por defecto. La forma más sencilla es una cadena, que se aplica a todos los protocolos.

<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;

$client = new Client([
    'proxy'   => 'http://USERNAME:PASSWORD@premium-residential.geonode.com:9000',
    'timeout' => 30,
]);

$response = $client->get('https://api.ipify.org?format=json');
echo $response->getBody(), PHP_EOL;

La forma de matriz permite elegir un proxy diferente por esquema URI y excluir hosts. Se reconocen tres claves: http, https y no.

<?php
$proxyUrl = 'http://USERNAME:PASSWORD@premium-residential.geonode.com:9000';

$client = new GuzzleHttp\Client([
    'proxy' => [
        'http'  => $proxyUrl,            // used for http:// targets
        'https' => $proxyUrl,            // used for https:// targets (CONNECT tunnel)
        'no'    => ['localhost', '.internal.example'], // never proxied
    ],
]);

Utiliza el formato de cadena cuando todo deba pasar por un único proxy. Utiliza el formato de matriz cuando necesites la lista no o quieras que las solicitudes HTTP sin cifrar pasen por un grupo de servidores de un centro de datos, mientras que las HTTPS se dirijan a servidores residenciales.

La lista no y la advertencia sobre NO_PROXY

Guzzle lee HTTP_PROXY, HTTPS_PROXY y NO_PROXY del entorno cuando no se configura ninguna opción proxy. En el momento en que se configura una, esa automatización se detiene, incluida la lista de exclusión. La documentación oficial deja claro que, en ese caso, es responsabilidad del usuario pasar no por su cuenta. Si tu contenedor configura NO_PROXY para servicios internos, olvidarte de esto significa que esas llamadas se redirigirán de repente a través del proxy residencial.

$no = getenv('NO_PROXY') ? explode(',', getenv('NO_PROXY')) : [];

$client = new GuzzleHttp\Client([
    'proxy' => [
        'http'  => $proxyUrl,
        'https' => $proxyUrl,
        'no'    => $no,
    ],
]);

Autenticación mediante proxy en Guzzle: credenciales en la URL

Guzzle no dispone de una opción proxy_auth independiente. Las credenciales se incluyen en la URL del proxy como userinfo (http://username:password@host:port), y el controlador cURL las envía como Proxy-Authorization: Basic. De ello se derivan dos reglas prácticas.

En primer lugar, codifica en formato «%» cualquier carácter de la contraseña que no sea seguro para URL. Un @, : o / en una contraseña sin procesar impide el análisis de la URL y provoca un confuso error 407 o una conexión al host equivocado. Construye la URL con rawurlencode() en lugar de mediante concatenación de cadenas.

function proxyUrl(string $user, string $pass, int $port = 9000): string
{
    return sprintf(
        'http://%s:%s@premium-residential.geonode.com:%d',
        rawurlencode($user),
        rawurlencode($pass),
        $port
    );
}

En segundo lugar, mantén las credenciales fuera del código. Léelas desde variables de entorno o desde un gestor de secretos y nunca registres la URL completa del proxy; la salida de Guzzle (debug) incluye el encabezado Proxy-Authorization, así que ocúltalo antes de enviar los registros a cualquier lugar.

Destinos HTTPS a través de un proxy HTTP CONNECT

Un punto habitual de confusión: el esquema de URL del proxy describe cómo te comunicas con el proxy, no qué utiliza el destino. En el caso de un destino https://, Guzzle (a través de cURL) abre una conexión HTTP simple con el proxy, envía CONNECT host:443 y, a continuación, realiza el protocolo de enlace TLS con el destino dentro de ese túnel. Por lo tanto, tanto la clave http como la https de la matriz suelen apuntar a una URL de proxy http://. Escribir https://premium-residential.geonode.com:9000 indicaría a cURL que envolviera el propio salto del proxy en TLS, algo que el punto final no espera.

Dado que TLS se ejecuta de extremo a extremo dentro del túnel, el proxy nunca ve ni altera el cuerpo de la respuesta, y la verificación habitual del certificado se aplica al sitio de destino exactamente igual que lo haría sin un proxy.

verify y problemas con los certificados

La opción verify controla las comprobaciones de certificados para el destino. Déjela en el valor predeterminado true, que utiliza el paquete de CA del sistema. Si PHP no encuentra ningún paquete (algo habitual en imágenes mínimas de Docker), configure verify para que apunte a un archivo PEM en lugar de desactivarlo.

$client = new GuzzleHttp\Client([
    'proxy'  => $proxyUrl,
    'verify' => '/etc/ssl/certs/ca-certificates.crt', // or true for the system default
]);

Configurar verify => false para silenciar un error que solo aparecía tras añadir un proxy es un error; un túnel CONNECT configurado correctamente no provoca errores de certificado, por lo que el verdadero problema está en otra parte, normalmente en la confusión de esquemas de https:// mencionada anteriormente.

Tiempos de espera: timeout y connect_timeout

El valor predeterminado de Guzzle para timeout es 0, lo que significa «esperar indefinidamente». A través de un proxy residencial, ese valor predeterminado no es adecuado, ya que un dispositivo de salida lento puede bloquear un trabajador indefinidamente. Establece dos valores: connect_timeout limita la conexión TCP al proxy y solo es compatible con el controlador cURL; timeout limita toda la transferencia en segundos.

$client = new GuzzleHttp\Client([
    'proxy'           => $proxyUrl,
    'connect_timeout' => 10,   // seconds to reach the proxy
    'timeout'         => 45,   // total seconds for the request
]);

Las salidas residenciales son más lentas que las de los centros de datos, por lo que conviene establecer un tiempo de espera total más amplio que el que se utilizaría para una solicitud directa. Un tiempo de espera de conexión que se agota constantemente suele indicar un problema en la ruta de red, más que en el destino.

Gestión de excepciones desde un proxy de Guzzle

Guzzle lanza excepciones tipadas, todas ellas subclases de GuzzleHttp\Exception\RequestException, y saber cuál se ha producido te indica dónde se ha producido el fallo.

ExcepciónCuándo se lanzaCausa típica del proxy
ConnectExceptionAusencia total de respuesta HTTPHost del proxy inaccesible, puerto incorrecto, tiempo de espera de conexión agotado, fallo de DNS
ClientExceptionRespuesta 4xx407: credenciales incorrectas; 403/429 del destino
ServerExceptionRespuesta 5xxError del destino o el proxy no ha podido acceder al destino
RequestExceptionTodo lo demásTiempo de espera agotado tras los encabezados, fallo de TLS

Merece la pena aislar un código 407. Es el proxy, y no el destino, el que rechaza la solicitud, por lo que volver a intentarlo no servirá de nada hasta que se cambien las credenciales.

<?php
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\RequestException;

try {
    $response = $client->get('https://example.com/');
} catch (ConnectException $e) {
    // Could not reach the proxy or the connect_timeout expired.
    error_log('connect: ' . $e->getMessage());
} catch (ClientException $e) {
    $status = $e->getResponse()->getStatusCode();
    if ($status === 407) {
        // Proxy rejected credentials: check username grammar and password.
        throw new RuntimeException('proxy auth failed', 407, $e);
    }
    // 403, 404, 429 etc. come from the target site.
} catch (ServerException $e) {
    // 5xx from the target (or the proxy failing to reach it).
} catch (RequestException $e) {
    // Timeouts after connect, TLS errors and other transfer failures.
}

Desactivar las excepciones con http_errors

Si prefieres ramificar en función del código de estado en lugar de capturar excepciones, configura http_errors => false. De este modo, Guzzle devolverá la respuesta para cualquier estado, incluido el 407, y tú mismo comprobarás getStatusCode(). ConnectException sigue lanzándose porque no hay ninguna respuesta que devolver.

$response = $client->get('https://example.com/', ['http_errors' => false]);

if ($response->getStatusCode() === 407) {
    // handle auth failure without an exception
}

Diagnóstico: on_stats y debug

Cuando una solicitud del proxy Guzzle es lenta o falla de forma intermitente, hay dos opciones que te indican qué ha ocurrido en la conexión. on_stats recibe un objeto GuzzleHttp\TransferStats tras cada transferencia, con el tiempo total y las estadísticas del gestor cURL, como la búsqueda de nombres, la conexión y los tiempos de TLS.

use GuzzleHttp\TransferStats;

$client->get('https://example.com/', [
    'on_stats' => function (TransferStats $stats) {
        printf("%s took %.3fs\n", $stats->getEffectiveUri(), $stats->getTransferTime());
        if (!$stats->hasResponse()) {
            var_dump($stats->getHandlerErrorData());
        }
    },
]);

debug => true habilita CURLOPT_VERBOSE y muestra el intercambio CONNECT, incluida la línea de respuesta del proxy. Úsalo una vez para confirmar que el túnel está establecido y, a continuación, elimínalo: escribe las credenciales en STDOUT. Al pasar un flujo fopen() en lugar de true, la salida se redirige a un archivo.

Sesiones fijas en el puerto 10000

El puerto 9000 cambia la dirección IP de salida en cada solicitud. Para flujos de varios pasos (iniciar sesión, a continuación cargar páginas y, por último, paginar), se necesita la misma dirección IP, que es lo que ofrece el puerto 10000. La sesión se selecciona mediante el nombre de usuario, no a través de un encabezado: añade «-session-XXXXXXXX» seguido de un identificador alfanumérico de 8 caracteres y, opcionalmente, «-lifetime-SECONDS» con un valor comprendido entre 180 y 86 400. Los sufijos de segmentación, como -country-us, se incluyen en la misma cadena.

<?php
function stickyProxyUrl(string $baseUser, string $pass, string $country = 'us', int $lifetime = 600): string
{
    $session = substr(bin2hex(random_bytes(4)), 0, 8); // 8 alphanumeric chars
    $user = sprintf('%s-country-%s-session-%s-lifetime-%d', $baseUser, $country, $session, $lifetime);

    return sprintf(
        'http://%s:%s@premium-residential.geonode.com:10000',
        rawurlencode($user),
        rawurlencode($pass)
    );
}

$client = new GuzzleHttp\Client([
    'proxy'   => stickyProxyUrl(getenv('GEONODE_USER'), getenv('GEONODE_PASS')),
    'timeout' => 45,
]);

Ahora, todas las solicitudes procedentes de $client comparten una misma IP de salida durante diez minutos. Crea un nuevo cliente (o pasa una nueva opción proxy) cuando desees una IP diferente.

Comprobación de la IP de salida

Antes de dar por válida una sesión, confirma lo que verá el destinatario. Realiza dos veces una llamada a un servicio de eco de IP a través del mismo cliente: con una sesión persistente, ambas llamadas devuelven la misma dirección; con el puerto 9000, las direcciones difieren.

$first  = json_decode((string) $client->get('https://api.ipify.org?format=json')->getBody(), true);
$second = json_decode((string) $client->get('https://api.ipify.org?format=json')->getBody(), true);

printf("exit IP: %s (stable: %s)\n", $first['ip'], $first['ip'] === $second['ip'] ? 'yes' : 'no');

Un pequeño ayudante de reintentos que cambia la sesión

Las conexiones residenciales pueden desaparecer a mitad de sesión y, en ocasiones, los servidores de destino bloquean una dirección IP concreta. Un enfoque pragmático consiste en reintentar una solicitud fallida con un nuevo identificador de sesión, lo que cambia la IP de salida y evita reutilizar una conexión que ya se encuentra en mal estado. La función de ayuda que se muestra a continuación recrea el cliente en cada intento y solo vuelve a intentarlo en caso de fallos de conexión, tiempos de espera agotados y códigos de estado que indiquen bloqueo.

<?php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;

function fetchWithNewSession(string $url, int $attempts = 3): ResponseInterface
{
    $lastError = null;

    for ($i = 1; $i <= $attempts; $i++) {
        $client = new Client([
            'proxy'           => stickyProxyUrl(getenv('GEONODE_USER'), getenv('GEONODE_PASS')),
            'connect_timeout' => 10,
            'timeout'         => 45,
            'http_errors'     => false,
        ]);

        try {
            $response = $client->get($url);
            $status   = $response->getStatusCode();

            if ($status === 407) {
                throw new RuntimeException('proxy credentials rejected'); // do not retry
            }
            if (!in_array($status, [403, 429, 502, 503, 504], true)) {
                return $response;
            }
            $lastError = new RuntimeException("status $status");
        } catch (ConnectException | RequestException $e) {
            $lastError = $e;
        }

        usleep(500_000 * $i); // simple linear backoff
    }

    throw new RuntimeException("gave up after $attempts attempts", 0, $lastError);
}

El mismo código funciona con el puerto 9000 si se omite el sufijo de sesión, ya que, de todos modos, cada intento obtiene una nueva IP. Mantén bajo el recuento de intentos: un destino que responde con un 403 tres veces seguidas te está dando una pista sobre la huella digital de la solicitud, no sobre el proxy.

Errores comunes

Uso de un esquema https:// en la URL del proxy

El salto del proxy es HTTP sin cifrar; el HTTPS del destino se ejecuta dentro del túnel CONNECT. Ambas claves de la matriz deben contener una URL de proxy http://. El síntoma habitual es un ConnectException que menciona un protocolo de enlace SSL con el host del proxy.

Olvidar la lista no tras configurar proxy

Una vez que se pasa proxy, se ignora NO_PROXY. Las comprobaciones de estado internas, los puntos finales de metadatos y las bases de datos a las que se accede a través de HTTP pasarán por el proxy residencial y fallarán o, lo que es peor, se producirán fugas. Pasa siempre no de forma explícita.

Dejar timeout en cero

Guzzle espera indefinidamente por defecto. Una sola conexión residencial bloqueada puede retener a un trabajador de la cola hasta que un supervisor lo elimine. Configura tanto connect_timeout como timeout en cada cliente proxy.

Reintentar un error 407

Un error 407 significa que el proxy ha rechazado las credenciales. Reintentarlo con el mismo nombre de usuario y contraseña solo es una pérdida de tiempo. Detecta el error rápidamente, registra el nombre de usuario (nunca la contraseña) y comprueba los sufijos gramaticales.

Siguiente paso

La configuración anterior funciona sin modificaciones con los proxies residenciales Geonode: 2,5 millones de direcciones IP activas en más de 195 países, HTTP, HTTPS y SOCKS5 en el mismo servidor, y sesiones persistentes de hasta 24 horas seleccionadas exclusivamente a través del nombre de usuario. La segmentación por país, provincia, ciudad y ASN utiliza la misma sintaxis que se muestra aquí, por lo que no es necesario modificar el código al restringir la ubicación de salida. Los planes son pay-as-you-go sin compromiso de permanencia, a partir de 10 GB por 7,92 $ al mes, y una prueba de 3 días cuesta 5 $. Consulta los proxies residenciales para obtener todos los detalles, o compara los distintos niveles en la página de precios. Para patrones de scraping que vayan más allá de la configuración de proxies, el caso de uso de web scraping cubre el resto del proceso.

Preguntas frecuentes

¿Cómo configuro un proxy de Guzzle para todas las solicitudes de un cliente?

Pasa la opción de proxy en el constructor del cliente en lugar de hacerlo por cada solicitud. new Client(['proxy' => 'http://user:pass@host:port']) aplica el proxy a todas las solicitudes que envía ese cliente, y cualquier opción de proxy a nivel de solicitud lo anula. Este es el patrón habitual cuando todo el tráfico de un trabajador debe salir a través del mismo proveedor.

¿Por qué mi proxy de Guzzle funciona con las URL http:// pero falla con los destinos https://?

Normalmente, falta la clave https en el formato de matriz o apunta a un esquema https://. Guzzle canaliza los destinos HTTPS a través de un proxy HTTP con CONNECT, por lo que la URL del proxy para la clave https debe seguir empezando por http://. Comprueba el formulario de matriz y asegúrate de que el puerto del proxy acepta CONNECT, como es el caso de los puertos Geonode.

¿Qué significa un error 407 «Proxy Authentication Required» en Guzzle?

El proxy ha rechazado tus credenciales antes de reenviar nada. Guzzle lo muestra como un ClientException con estado 407 porque se trata de una respuesta 4xx. Comprueba la sintaxis del nombre de usuario, la contraseña y que los caracteres especiales de la contraseña estén codificados con el símbolo de porcentaje en la URL. Si se establece http_errors en «false», obtendrás el objeto de respuesta 407 en lugar de una excepción.

¿Respeta Guzzle las variables de entorno HTTP_PROXY y NO_PROXY?

Sí, cuando no se pasa una opción de proxy, Guzzle lee HTTP_PROXY, HTTPS_PROXY y NO_PROXY del entorno. En el momento en que pasas tú mismo una opción de proxy, se detiene el manejo automático de NO_PROXY, por lo que debes pasar la clave no de forma explícita, normalmente explode(',', getenv('NO_PROXY')).

¿Puedo mantener la misma IP de salida en varias solicitudes de Guzzle?

Sí, utiliza una sesión persistente. Configura el proxy de Guzzle en el puerto 10000 y añade -session-XXXXXXXX (8 caracteres alfanuméricos) más un sufijo opcional -lifetime-SECONDS al nombre de usuario. Cada solicitud que reutilice ese nombre de usuario compartirá una misma IP de salida durante el tiempo de vida que hayas establecido, entre 180 segundos y 24 horas.

¿Hay algún límite que deba tener en cuenta al realizar scraping a través de un proxy de Guzzle?

El ancho de banda residencial se factura por gigabyte, por lo que las páginas HTML e imágenes de gran tamaño acumulan datos. Las sesiones persistentes duran como máximo 24 horas y pueden finalizar antes si el dispositivo de origen se desconecta, por lo que el código debe tolerar un cambio en la IP de salida. Respeta también los términos y condiciones, las reglas para robots y los límites de frecuencia del sitio de destino; un proxy no modifica tus obligaciones legales.