Base de connaissances

Comment utiliser des proxys résidentiels en PHP avec cURL et Guzzle

Les proxys en PHP : CURLOPT_PROXY et CURLOPT_PROXYUSERPWD de cURL, l'option proxy et la réutilisation de connexion de Guzzle, CURLOPT_HTTPPROXYTUNNEL, et la rotation géo par requête.

Chris Collins

Chris Collins

26 juillet 2026 · 10 min de lecture

PHP déplace toujours une énorme part du trafic serveur-à-serveur du web, et l’essentiel passe par l’un de deux chemins : cURL brut, ou Guzzle posé au-dessus de cURL. Brancher un proxy résidentiel dans l’un ou l’autre, c’est quelques options, pas une réécriture. La friction est dans les détails que chacun gère différemment : comment l’authentification du proxy est fournie, un réglage qui décide si HTTPS à travers le proxy fonctionne tout court, et le comportement de réutilisation de connexion qui sépare un scraper rapide d’un scraper qui paie un handshake complet à chaque requête.

Ceci est l’entrée PHP de la même série que proxys résidentiels avec Python, avec Playwright, et avec Go : le code qui marche pour cURL et pour Guzzle, plus les pièges spécifiques à PHP.

Tout ci-dessous utilise le gateway résidentiel de Shifter : un point de terminaison, p.shifter.io:443, avec tout le ciblage encodé dans le nom d’utilisateur. Changez l’hôte et les identifiants pour un autre fournisseur ; la forme est la même.

Le modèle du gateway en un paragraphe

Le nom d’utilisateur du proxy porte votre authentification et votre ciblage. Vous ne changez pas de point de terminaison pour changer de pays ou de session, vous changez la chaîne du nom d’utilisateur :

customer-USERNAME-country-us-sid-abc123-ttl-600

country-us cible les États-Unis, sid fixe une session sticky, ttl maintient cette IP pendant N secondes. Omettez sid/ttl et chaque nouvelle connexion tourne. Le mot de passe est constant. Cette chaîne entière est le nom d’utilisateur que vous passez à cURL ou à Guzzle.

cURL brut

cURL prend l’hôte du proxy dans CURLOPT_PROXY et les identifiants dans CURLOPT_PROXYUSERPWD. Vous pouvez aussi mettre user:pass@host en ligne dans la chaîne du proxy, mais garder les identifiants dans leur propre option est plus propre et évite les maux de tête d’encodage URL avec le long nom d’utilisateur.

<?php
$user = getenv('SHIFTER_USER') . '-country-us';
$pass = getenv('SHIFTER_PASS');
$ch = curl_init('https://api.ipify.org');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'p.shifter.io:443',
CURLOPT_PROXYUSERPWD => "$user:$pass",
CURLOPT_HTTPPROXYTUNNEL => true, // tunnel CONNECT pour HTTPS à travers le proxy
CURLOPT_CONNECTTIMEOUT => 10, // phase de connexion seulement
CURLOPT_TIMEOUT => 30, // transfert complet
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, 'curl error: ' . curl_error($ch) . "\n");
} else {
echo $body, "\n"; // une IP résidentielle américaine
}
curl_close($ch);

Deux choses à intérioriser. La chaîne $user inclut les flags de ciblage (-country-us), parce que c’est là que vit la géo. Et CURLOPT_HTTPPROXYTUNNEL est ce qui fait marcher HTTPS-à-travers-un-proxy : il dit à cURL d’ouvrir un tunnel CONNECT pour que TLS soit négocié de bout en bout avec la cible, pas avec le proxy. Laissez-le désactivé et les requêtes HTTPS à travers un proxy HTTP échouent ou se comportent bizarrement. C’est de loin l’erreur de proxy la plus courante en PHP.

Guzzle

Guzzle prend le proxy via l’option proxy de la requête (ou du client), les identifiants en ligne dans l’URL. Comme Guzzle est un wrapper de cURL, le même comportement de tunnel s’applique en dessous, mais Guzzle gère le CONNECT pour les cibles https:// automatiquement.

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$user = getenv('SHIFTER_USER') . '-country-us';
$pass = getenv('SHIFTER_PASS');
$proxy = "http://$user:$pass@p.shifter.io:443";
// Construisez le client UNE fois et réutilisez-le (voir la note sur la réutilisation de connexion ci-dessous).
$client = new Client([
'proxy' => $proxy,
'connect_timeout' => 10, // phase de connexion
'timeout' => 30, // requête complète
]);
$res = $client->get('https://api.ipify.org');
echo $res->getBody(), "\n"; // une IP résidentielle américaine

Notez les connect_timeout et timeout séparés. Cette séparation connexion-versus-total est exactement ce qui rend les timeouts diagnosticables quand quelque chose bloque : une connexion lente pointe vers votre côté ou une IP correspondante manquante, un total lent pointe vers la cible.

Guzzle accepte aussi l’option proxy par requête et sous forme de tableau indexé par schéma, c’est ainsi que vous routez seulement https à travers le proxy ou définissez une liste no d’exceptions :

$res = $client->get('https://example.com', [
'proxy' => [
'http' => $proxy,
'https' => $proxy,
'no' => ['localhost', '127.0.0.1'],
],
]);

Piège 1 : réutilisez le client Guzzle (et le handle cURL)

Un GuzzleHttp\Client frais par requête, ou un curl_init() frais par requête, paie un handshake TCP + TLS complet à travers le proxy à chaque fois, le surcoût que le guide de latence existe pour éliminer. Guzzle maintient un pool de handles cURL en dessous et réutilise les connexions quand vous réutilisez le client. Construisez un client au démarrage, injectez-le, et gardez-le.

Pour cURL brut, réutilisez le handle entre les requêtes et ne changez que l’URL entre les appels, pour que la connexion reste chaude :

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_PROXY => 'p.shifter.io:443',
CURLOPT_PROXYUSERPWD => "$user:$pass",
CURLOPT_HTTPPROXYTUNNEL => true,
]);
foreach ($urls as $url) {
curl_setopt($ch, CURLOPT_URL, $url); // réutilise le handle, garde la connexion
$body = curl_exec($ch);
// ... traiter $body
}
curl_close($ch);

Piège 2 : par défaut PHP récupère une URL à la fois

Les gestionnaires de requêtes PHP sont synchrones par défaut. Un scraper qui boucle sur curl_exec récupère strictement une URL à la fois, ce qui convient aux petits travaux et est douloureusement lent à l’échelle. Deux façons de monter :

Guzzle asynchrone avec un pool borné, pour que N requêtes soient en vol mais jamais plus :

use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
$requests = function ($urls) {
foreach ($urls as $u) { yield new Request('GET', $u); }
};
$pool = new Pool($client, $requests($urls), [
'concurrency' => 8, // borne les requêtes en vol
'fulfilled' => function ($response, $i) { /* traiter */ },
'rejected' => function ($reason, $i) { /* log + réessai */ },
]);
$pool->promise()->wait();

Ou curl_multi_* si vous êtes sur cURL brut. Dans tous les cas, bornez la concurrence par hôte cible, pas globalement, pour qu’un site fragile ne soit pas martelé pendant qu’un permissif est affamé. Plus de parallélisme au-delà de la tolérance d’une cible achète des blocages, pas du débit (comment éviter de se faire bloquer).

Piège 3 : consommez la réponse, et vérifiez l’erreur de transport, pas seulement le statut

cURL renvoie false sur un échec de transport (proxy refusé, tunnel échoué, timeout) et une chaîne de body sur une réponse HTTP, même un 407 ou un 502. Vérifiez le retour de curl_exec contre false et lisez curl_error/curl_errno avant de vous fier au code de statut. En Guzzle, un échec de connexion lève une ConnectException tandis qu’un 4xx/5xx ne lève une RequestException que si http_errors est activé (il l’est, par défaut). Gérez les deux, et lisez toujours le body pour que le handle soit libre à réutiliser :

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
try {
$res = $client->get($url);
$body = (string) $res->getBody(); // vide le body
} catch (ConnectException $e) {
// transport : proxy/tunnel/timeout — réessayer avec une identité fraîche
} catch (RequestException $e) {
// statut HTTP — inspecter $e->getResponse()->getStatusCode()
}

Faire tourner géo et sessions

Parce que le ciblage vit dans le nom d’utilisateur, une identité différente est une chaîne d’identifiants différente.

cURL brut : posez CURLOPT_PROXYUSERPWD avec le nouveau nom d’utilisateur avant l’appel. Le même handle peut porter des identités différentes entre les itérations.

Guzzle : passez l’option proxy par requête pour écraser le défaut du client, pour qu’un client serve plusieurs identités sans reconstruction :

function userFor(string $country, ?string $sid = null): string {
$u = getenv('SHIFTER_USER') . '-country-' . $country;
if ($sid !== null) { $u .= '-sid-' . $sid . '-ttl-600'; }
return $u;
}
$pass = getenv('SHIFTER_PASS');
$proxy = 'http://' . userFor('de', 'job-42') . ":$pass@p.shifter.io:443";
$res = $client->get('https://example.com', ['proxy' => $proxy]);

Donnez à chaque unité logique de travail son propre sid et tournez entre unités, pas en milieu de flux (sticky vs rotatif couvre la distinction), et mappez le travail aux identités comme le décrit le billet sur la répartition de charge.

Vérifiez que vous êtes bien sur le proxy

Avant de benchmarker ou de déboguer quoi que ce soit d’autre, confirmez l’IP de sortie :

$res = $client->get('http://ip-api.com/json');
echo $res->getBody(), "\n"; // attendez une IP résidentielle dans le pays ciblé

Votre propre IP signifie que l’option du proxy n’est pas appliquée. Un blocage signifie que la sortie locale est bloquée. Les deux sont couverts dans le guide de diagnostic des timeouts.

FAQ

Pourquoi mes requêtes HTTPS échouent-elles à travers le proxy en cURL brut ? Il vous manque presque certainement CURLOPT_HTTPPROXYTUNNEL. HTTPS à travers un proxy HTTP a besoin d’un tunnel CONNECT pour que TLS soit négocié avec la cible, pas avec le proxy. Posez CURLOPT_HTTPPROXYTUNNEL => true. Guzzle le fait automatiquement pour les cibles https://, c’est pourquoi le piège ne mord que le cURL brut.

Dois-je mettre les identifiants du proxy dans l’URL ou dans une option séparée ? Les deux marchent. En cURL brut, CURLOPT_PROXYUSERPWD garde le long nom d’utilisateur du gateway hors de l’URL et évite les problèmes d’encodage URL. En Guzzle, mettre user:pass@host en ligne dans la chaîne proxy est le chemin normal. Choisissez-en un et restez cohérent.

Mon scraper PHP est lent alors que le proxy est rapide. Pourquoi ? Le plus probable : vous créez un nouveau client Guzzle (ou curl_init) par requête, payant un handshake frais à chaque fois, ou vous récupérez strictement une URL à la fois. Réutilisez un client/handle, et utilisez un Pool Guzzle borné ou curl_multi pour lancer plusieurs requêtes en parallèle.

Comment faire tourner les IP par requête en PHP ? Variez le nom d’utilisateur du proxy. En cURL brut, posez CURLOPT_PROXYUSERPWD avec le nouveau nom d’utilisateur avant chaque appel. En Guzzle, passez l’option proxy par requête. Les deux laissent un client ou un handle partagé servir plusieurs identités.

cURL ou Guzzle pour le scraping ? Guzzle vous donne des pools asynchrones, du middleware, des réessais et PSR-7 pour une petite dépendance ; cURL brut est sans dépendance et un peu plus rapide par appel. Prenez Guzzle quand vous voulez de la concurrence et de la structure, cURL brut pour des scripts serrés et minimaux. La configuration du proxy et les pièges ci-dessus s’appliquent aux deux, puisque Guzzle tourne sur cURL.

En résumé

PHP plus proxys résidentiels est solide dès lors que vous respectez les règles de chaque client : en cURL brut, posez l’hôte du proxy et CURLOPT_PROXYUSERPWD, et n’oubliez jamais CURLOPT_HTTPPROXYTUNNEL pour HTTPS ; en Guzzle, passez l’option proxy et laissez-le tunneler pour vous. Réutilisez un client ou un handle pour que les connexions restent chaudes, lancez les requêtes en parallèle avec un pool borné plutôt qu’une à la fois, bornez la concurrence par hôte cible, et vérifiez l’erreur de transport, pas seulement le code de statut. Variez le nom d’utilisateur du proxy pour changer géo ou session.

Réussissez cela et les deux chemins performent comme PHP le devrait à l’échelle. Pointez-les vers le gateway résidentiel, et souvenez-vous que la qualité du pool décide à quelle fréquence vous réessayez tout court (réputation d’IP). La page tarifs propose les forfaits au Go pour le tester contre vos propres cibles.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de $0.75/GB.

Commencer