Ce guide passe en revue toutes les étapes d'une configuration fonctionnelle d'un proxy avec Guzzle : l'option de requête proxy sous ses formes chaîne de caractères et tableau, les URL de proxy authentifiées, les cibles HTTPS via un tunnel CONNECT, les délais d'expiration et la vérification des certificats, ainsi que les exceptions levées par Guzzle lorsque le proxy refuse l'accès. À la fin, vous disposerez d'un client PHP réutilisable qui achemine le trafic via un proxy résidentiel, conserve une seule adresse IP de sortie pendant toute la durée d'une session, vérifie cette adresse IP et effectue une nouvelle tentative avec une session vierge en cas d'échec d'une requête.
Tous les exemples sont destinés à Guzzle 7 et PHP 8.1 ou une version ultérieure. Installez-le avec composer require guzzlehttp/guzzle:^7.
L'option proxy : chaîne de caractères ou tableau
Guzzle configure un proxy via l'option de requête proxy, documentée sur docs.guzzlephp.org. Vous pouvez la passer à chaque requête ou dans le constructeur du client en tant que valeur par défaut. La forme la plus simple est une chaîne de caractères, qui s'applique à tous les protocoles.
<?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 forme tableau vous permet de choisir un proxy différent selon le schéma URI et d’exclure certains hôtes. Trois clés sont reconnues : http, https et 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
],
]);
Utilisez la forme chaîne de caractères lorsque tout doit passer par un seul proxy. Utilisez la forme tableau lorsque vous avez besoin de la liste no ou que vous souhaitez que les requêtes HTTP simples passent par un pool de centres de données tandis que les requêtes HTTPS passent par des connexions résidentielles.
La liste no et la mise en garde concernant NO_PROXY
Guzzle lit les variables HTTP_PROXY, HTTPS_PROXY et NO_PROXY dans l’environnement lorsque vous ne définissez pas d’option proxy. Dès que vous en définissez une, cette automatisation s’arrête, y compris la liste d’exclusion. La documentation officielle indique clairement qu’il vous appartient alors de passer vous-même no. Si votre conteneur définit NO_PROXY pour les services internes, oublier cette étape signifie que ces appels seront soudainement acheminés via le proxy résidentiel.
$no = getenv('NO_PROXY') ? explode(',', getenv('NO_PROXY')) : [];
$client = new GuzzleHttp\Client([
'proxy' => [
'http' => $proxyUrl,
'https' => $proxyUrl,
'no' => $no,
],
]);
Authentification via proxy avec Guzzle : identifiants dans l'URL
Guzzle ne dispose pas d'une option distincte « proxy_auth ». Les identifiants sont intégrés à l'URL du proxy sous la forme « userinfo » (http://username:password@host:port), et le gestionnaire cURL les envoie sous la forme Proxy-Authorization: Basic. Deux règles pratiques en découlent.
Tout d'abord, encodez en pourcentage tout élément du mot de passe qui n'est pas compatible avec les URL. La présence d’un @, : ou / dans un mot de passe brut empêche l’analyse de l’URL et génère une erreur 407 déroutante ou une connexion à un hôte incorrect. Construisez l’URL à l’aide de rawurlencode() plutôt que par concaténation de chaînes.
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
);
}
Deuxièmement, ne placez pas les identifiants dans le code. Lisez-les à partir de variables d’environnement ou d’un gestionnaire de secrets et n’enregistrez jamais l’URL complète du proxy ; la sortie de Guzzle (debug) inclut l’en-tête Proxy-Authorization, il faut donc le masquer avant d’envoyer les journaux où que ce soit.
Cibles HTTPS via un proxy HTTP CONNECT
Une source fréquente de confusion : le schéma d'URL du proxy décrit la manière dont vous communiquez avec le proxy, et non le protocole utilisé par la cible. Pour une cible https://, Guzzle (via cURL) ouvre une connexion HTTP simple vers le proxy, envoie CONNECT host:443, puis effectue la négociation TLS avec la cible à l'intérieur de ce tunnel. Ainsi, les clés « http » et « https » du tableau pointent normalement vers une URL de proxy « http:// ». Écrire « https://premium-residential.geonode.com:9000 » indiquerait à cURL d’envelopper le saut de proxy lui-même dans un tunnel TLS, ce à quoi le point de terminaison ne s’attend pas.
Comme le protocole TLS fonctionne de bout en bout à l’intérieur du tunnel, le proxy ne voit ni ne modifie jamais le corps de la réponse, et la vérification habituelle du certificat s’applique au site cible exactement comme si aucun proxy n’était utilisé.
verify et problèmes de certificats
L’option verify contrôle les vérifications de certificats pour la cible. Conservez la valeur par défaut true, qui utilise le bundle d’autorités de certification (CA) du système. Si PHP ne parvient pas à trouver de bundle (ce qui est courant sur les images Docker minimales), pointez verify vers un fichier PEM plutôt que de désactiver cette option.
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'verify' => '/etc/ssl/certs/ca-certificates.crt', // or true for the system default
]);
Définir verify => false pour masquer une erreur qui n'est apparue qu'après l'ajout d'un proxy est une erreur ; un tunnel CONNECT correctement configuré ne provoque pas d'erreurs de certificat, le véritable problème se situe donc ailleurs, généralement dans la confusion entre les schémas mentionnée ci-dessus pour https://.
Délais d'expiration : timeout et connect_timeout
La valeur par défaut de Guzzle pour timeout est 0, ce qui signifie « attendre indéfiniment ». Avec un proxy résidentiel, cette valeur par défaut n’est pas adaptée, car un dispositif de sortie lent peut bloquer indéfiniment un worker. Définissez deux valeurs : connect_timeout limite la connexion TCP vers le proxy et n’est pris en charge que par le gestionnaire cURL ; timeout limite l’ensemble du transfert en secondes.
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'connect_timeout' => 10, // seconds to reach the proxy
'timeout' => 45, // total seconds for the request
]);
Les points de sortie résidentiels sont plus lents que ceux des centres de données ; il faut donc prévoir un délai d'expiration total plus long que pour une requête directe. Un délai d'expiration de connexion qui se déclenche systématiquement indique un problème au niveau du chemin réseau plutôt qu'au niveau de la cible.
Gestion des exceptions provenant d'un proxy Guzzle
Guzzle lève des exceptions typées, qui héritent toutes de GuzzleHttp\Exception\RequestException ; le type de l'exception reçue permet de déterminer l'origine de l'échec.
| Exception | Quand elle est levée | Cause typique liée au proxy |
|---|
ConnectException | Aucune réponse HTTP | Hôte du proxy inaccessible, port incorrect, délai d'attente de connexion dépassé, échec DNS |
ClientException | Réponse 4xx | 407 : identifiants incorrects ; 403/429 de la part de la cible |
ServerException | Réponse 5xx | Erreur de la cible, ou le proxy n’a pas pu atteindre la cible |
RequestException | Tous les autres cas | Délai d'attente total après les en-têtes, échec TLS |
Un code 407 mérite d'être isolé. C'est le proxy, et non la cible, qui refuse la requête ; il est donc inutile de réessayer tant que les identifiants n'ont pas été modifiés.
<?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.
}
Désactiver les exceptions avec http_errors
Si vous préférez effectuer une branche en fonction du code d’état plutôt que d’intercepter les exceptions, définissez http_errors => false. Guzzle renvoie alors la réponse quel que soit le statut, y compris le 407, et vous vérifiez vous-même getStatusCode(). ConnectException est toujours levé car il n’y a pas de réponse à renvoyer.
$response = $client->get('https://example.com/', ['http_errors' => false]);
if ($response->getStatusCode() === 407) {
// handle auth failure without an exception
}
Diagnostics : on_stats et debug
Lorsqu'une requête proxy Guzzle est lente ou échoue de manière intermittente, deux options vous permettent de savoir ce qui s'est passé sur le réseau. on_stats reçoit un objet GuzzleHttp\TransferStats après chaque transfert, avec la durée totale et les statistiques du gestionnaire cURL telles que la recherche de nom, la connexion et les temps de réponse 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 active CURLOPT_VERBOSE et affiche l'échange CONNECT, y compris la ligne de réponse du proxy. Utilisez-le une seule fois pour vérifier que le tunnel est établi, puis supprimez-le : il écrit les identifiants sur STDOUT. Le fait de passer un flux fopen() à la place de true redirige la sortie vers un fichier.
Sessions persistantes sur le port 10000
Le port 9000 change d'adresse IP de sortie à chaque requête. Pour les flux en plusieurs étapes (connexion, puis consultation de pages, puis pagination), vous avez besoin d'une adresse IP fixe, ce que permet le port 10000. La session est sélectionnée via le nom d'utilisateur, et non via un en-tête : ajoutez à « -session-XXXXXXXX » un identifiant alphanumérique de 8 caractères, et éventuellement « -lifetime-SECONDS » compris entre 180 et 86 400. Les suffixes de ciblage tels que -country-us s’insèrent dans la même chaîne.
<?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,
]);
Chaque requête provenant de $client partage désormais une seule adresse IP de sortie pendant dix minutes. Créez un nouveau client (ou passez une nouvelle option proxy) lorsque vous souhaitez une adresse IP différente.
Vérification de l’adresse IP de sortie
Avant de valider une session, vérifiez ce que la cible verra. Effectuez deux fois une requête auprès d’un service d’écho IP via le même client : avec une session persistante, les deux appels renvoient la même adresse ; avec le port 9000, elles diffèrent.
$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 petit utilitaire de réessai qui change d'identifiant de session
Les adresses IP résidentielles peuvent disparaître en cours de session, et les cibles bloquent parfois une adresse IP spécifique. Une approche pragmatique consiste à réessayer une requête ayant échoué avec un nouvel identifiant de session, ce qui permet à la fois de changer l’adresse IP de sortie et d’éviter de réutiliser une connexion déjà en état d’erreur. L’utilitaire ci-dessous recrée le client à chaque tentative et ne réessaie qu’en cas d’échec de connexion, de délai d’attente dépassé et de codes d’état indiquant un blocage.
<?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);
}
Le même code fonctionne avec le port 9000 si vous supprimez le suffixe de session, puisque chaque tentative obtient de toute façon une nouvelle adresse IP. Limitez le nombre de tentatives : une cible qui renvoie un code 403 trois fois de suite vous donne des indications sur l’empreinte de la requête, et non sur le proxy.
Erreurs courantes
Utilisation d'un schéma https:// dans l'URL du proxy
Le saut de proxy s'effectue en HTTP simple ; le HTTPS de la cible s'exécute à l'intérieur du tunnel CONNECT. Les deux clés du tableau doivent contenir une URL de proxy au format http://. Le symptôme habituel est la présence d’un ConnectException mentionnant une négociation SSL avec l’hôte proxy.
Oubli de la liste no après avoir défini proxy
Une fois que vous avez passé proxy, NO_PROXY est ignoré. Les contrôles de santé internes, les points de terminaison de métadonnées et les bases de données accessibles via HTTP passeront par le proxy résidentiel et échoueront, ou pire, feront l’objet d’une fuite. Passez toujours no explicitement.
Laisser timeout à zéro
Par défaut, Guzzle attend indéfiniment. Une seule connexion résidentielle bloquée peut immobiliser un travailleur de file d’attente jusqu’à ce qu’un superviseur le force à s’arrêter. Définissez à la fois connect_timeout et timeout sur chaque client proxy.
Réessayer après un code 407
Un code 407 signifie que le proxy a rejeté les identifiants. Réessayer avec le même nom d’utilisateur et le même mot de passe ne fait que perdre du temps. Interrompez rapidement la tentative, consignez le nom d’utilisateur (jamais le mot de passe) et vérifiez les suffixes de syntaxe.
Étape suivante
La configuration ci-dessus fonctionne telle quelle avec les proxys résidentiels Geonode : 2,5 millions d’adresses IP en ligne dans plus de 195 pays, HTTP, HTTPS et SOCKS5 sur le même hôte, ainsi que des sessions persistantes pouvant aller jusqu’à 24 heures, sélectionnées uniquement via le nom d’utilisateur. Le ciblage par pays, région, ville et ASN utilise la même syntaxe que celle présentée ici ; aucune modification du code n'est donc nécessaire lorsque vous affinez la localisation de sortie. Les forfaits sont disponibles sous la référence pay-as-you-go sans engagement, à partir de 10 Go pour 7,92 $ par mois, et un essai de 3 jours coûte 5 $. Consultez la page proxys résidentiels pour plus de détails, ou comparez les formules sur la page des tarifs. Pour les scénarios de scraping allant au-delà de la configuration des proxys, le cas d'utilisation du web scraping couvre le reste du pipeline.
FAQ
Comment configurer un proxy Guzzle pour chaque requête provenant d’un même client ?
Passez l’option de proxy dans le constructeur du client plutôt que pour chaque requête. new Client(['proxy' => 'http://user:pass@host:port']) applique le proxy à toutes les requêtes envoyées par ce client, et toute option de proxy au niveau de la requête le remplace. C’est le schéma habituel lorsque tout le trafic provenant d’un worker doit passer par le même fournisseur.
Pourquoi mon proxy Guzzle fonctionne-t-il pour les URL de type http:// mais échoue-t-il pour les cibles de type https:// ?
En général, la clé https dans le tableau est manquante ou pointe vers un schéma https://. Guzzle achemine les cibles HTTPS via un proxy HTTP à l’aide de la méthode CONNECT ; par conséquent, l’URL du proxy associée à la clé https doit toujours commencer par http://. Vérifiez le tableau et assurez-vous que le port du proxy accepte la méthode CONNECT, ce qui est le cas des ports Geonode.
Que signifie une erreur 407 « Proxy Authentication Required » dans Guzzle ?
Le proxy a rejeté vos identifiants avant de transférer quoi que ce soit. Guzzle génère une erreur ClientException avec le statut 407, car il s’agit d’une réponse 4xx. Vérifiez la syntaxe du nom d’utilisateur, le mot de passe, et assurez-vous que les caractères spéciaux du mot de passe sont encodés en pourcentage dans l’URL. Si http_errors est défini sur « false », vous obtenez l’objet de réponse 407 au lieu d’une exception.
Guzzle prend-il en charge les variables d’environnement HTTP_PROXY et NO_PROXY ?
Oui, lorsque vous ne transmettez pas d’option de proxy, Guzzle lit les codes HTTP_PROXY, HTTPS_PROXY et NO_PROXY à partir de l’environnement. Dès que vous passez vous-même une option de proxy, la gestion automatique de NO_PROXY s'arrête ; vous devez donc passer explicitement la clé no, généralement explode(',', getenv('NO_PROXY')).
Puis-je conserver la même adresse IP de sortie pour plusieurs requêtes Guzzle ?
Oui, utilisez une session persistante. Configurez le proxy Guzzle sur le port 10000 et ajoutez -session-XXXXXXXX (8 caractères alphanumériques) ainsi qu’un suffixe -lifetime-SECONDS facultatif au nom d’utilisateur. Chaque requête réutilisant ce nom d’utilisateur partage une même adresse IP de sortie pendant la durée que vous avez définie, comprise entre 180 secondes et 24 heures.
Y a-t-il des limites à prendre en compte lors du scraping via un proxy Guzzle ?
La bande passante résidentielle est facturée au gigaoctet ; les pages HTML volumineuses et les images ont donc un impact important. Les sessions persistantes durent au maximum 24 heures et peuvent prendre fin plus tôt si le périphérique en amont se déconnecte ; le code doit donc s’adapter à un changement d’adresse IP de sortie. Respectez également les conditions d’utilisation, les règles relatives aux robots et les limites de débit du site cible ; un proxy ne modifie en rien vos obligations légales.