Dieser Leitfaden führt Sie durch alle Aspekte einer funktionierenden Guzzle-Proxy-Konfiguration: die Anfrageoption „proxy“ in ihrer String- und Array-Form, authentifizierte Proxy-URLs, HTTPS-Ziele über einen CONNECT-Tunnel, Timeouts und Zertifikatsüberprüfung sowie die Ausnahmen, die Guzzle auslöst, wenn der Proxy den Zugriff verweigert. Am Ende verfügen Sie über einen wiederverwendbaren PHP-Client, der über einen Residential-Proxy leitet, während einer gesamten Sitzung eine einzige Ausgangs-IP beibehält, diese IP überprüft und bei einem Fehlschlag der Anfrage mit einer neuen Sitzung einen erneuten Versuch unternimmt.
Alle Beispiele sind für Guzzle 7 und PHP 8.1 oder höher ausgelegt. Installieren Sie das Programm mit composer require guzzlehttp/guzzle:^7.
Die Option „proxy“: Zeichenkette oder Array
Guzzle konfiguriert einen Proxy über die Anfrageoption „proxy“, die unter docs.guzzlephp.org dokumentiert ist. Sie können diese Option pro Anfrage oder im Client-Konstruktor als Standardwert übergeben. Die einfachste Form ist eine Zeichenkette, die für jedes Protokoll gilt.
<?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;
Die Array-Form ermöglicht es Ihnen, je nach URI-Schema einen anderen Proxy auszuwählen und Hosts auszuschließen. Es werden drei Schlüssel erkannt: http, https und 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
],
]);
Verwenden Sie die String-Form, wenn alles über einen einzigen Proxy laufen soll. Verwenden Sie die Array-Form, wenn Sie die no-Liste benötigen oder wenn einfache HTTP-Anfragen über einen Rechenzentrums-Pool laufen sollen, während HTTPS über private Verbindungen geleitet wird.
Die no-Liste und der Hinweis zu NO_PROXY
Guzzle liest HTTP_PROXY, HTTPS_PROXY und NO_PROXY aus der Umgebung aus, wenn Sie keine proxy-Option festlegen. Sobald Sie eine solche Option festlegen, wird diese Automatisierung beendet, einschließlich der Ausschlussliste. In der offiziellen Dokumentation wird ausdrücklich darauf hingewiesen, dass es dann an Ihnen liegt, no selbst zu übergeben. Wenn Ihr Container NO_PROXY für interne Dienste festlegt, führt das Versäumen dieser Maßnahme dazu, dass diese Aufrufe plötzlich über den Proxy für Privatanwender geleitet werden.
$no = getenv('NO_PROXY') ? explode(',', getenv('NO_PROXY')) : [];
$client = new GuzzleHttp\Client([
'proxy' => [
'http' => $proxyUrl,
'https' => $proxyUrl,
'no' => $no,
],
]);
Guzzle-Proxy-Authentifizierung: Anmeldedaten in der URL
Guzzle verfügt über keine separate Option proxy_auth. Anmeldedaten werden als userinfo (http://username:password@host:port) in die Proxy-URL eingefügt, und der cURL-Handler übermittelt sie als Proxy-Authorization: Basic. Daraus ergeben sich zwei praktische Regeln.
Erstens: Führen Sie eine Prozent-Kodierung für alle Teile des Passworts durch, die nicht URL-sicher sind. Ein @, : oder / im Rohpasswort stört die URL-Auswertung und führt zu einem verwirrenden 407-Fehler oder einer Verbindung zum falschen Host. Erstellen Sie die URL mit rawurlencode() statt durch Zeichenfolgenverkettung.
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
);
}
Zweitens: Halten Sie Anmeldedaten aus dem Code fern. Lesen Sie sie aus Umgebungsvariablen oder einem Secrets-Manager aus und protokollieren Sie niemals die vollständige Proxy-URL; die Ausgabe von Guzzle (debug) enthält den Header Proxy-Authorization, daher sollten Sie diesen entfernen, bevor Sie Protokolle irgendwohin senden.
HTTPS-Ziele über einen HTTP-CONNECT-Proxy
Ein häufiger Irrtum: Das Proxy-URL-Schema beschreibt, wie man mit dem Proxy kommuniziert, nicht, was das Ziel verwendet. Bei einem Ziel vom Typ „https://“ öffnet Guzzle (über cURL) eine einfache HTTP-Verbindung zum Proxy, sendet „CONNECT host:443“ und führt dann den TLS-Handshake mit dem Ziel innerhalb dieses Tunnels durch. Daher verweisen sowohl der Schlüssel „http“ als auch „https“ im Array normalerweise auf eine http://-Proxy-URL. Würde man „https://premium-residential.geonode.com:9000“ schreiben, würde dies cURL anweisen, den Proxy-Hop selbst in TLS einzubinden, was der Endpunkt nicht erwartet.
Da TLS innerhalb des Tunnels durchgehend (End-to-End) ausgeführt wird, sieht der Proxy den Antworttext nie und verändert ihn auch nicht, und Ihre normale Zertifikatsüberprüfung gilt für die Zielseite genau so, wie es ohne Proxy der Fall wäre.
verify und Zertifikatsprobleme
Die Option „verify“ steuert die Zertifikatsprüfungen für das Ziel. Belassen Sie die Standardeinstellung true, die das System-CA-Bundle verwendet. Wenn PHP kein Bundle finden kann (was bei minimalen Docker-Images häufig vorkommt), verweisen Sie verify auf eine PEM-Datei, anstatt die Option zu deaktivieren.
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'verify' => '/etc/ssl/certs/ca-certificates.crt', // or true for the system default
]);
Es ist ein Fehler, verify => false zu setzen, um einen Fehler zu unterdrücken, der erst nach dem Hinzufügen eines Proxys aufgetreten ist; ein korrekt konfigurierter CONNECT-Tunnel verursacht keine Zertifikatsfehler, daher liegt das eigentliche Problem woanders, in der Regel bei der oben genannten Verwechslung der Schemata unter https://.
Timeouts: timeout und connect_timeout
Der Standardwert für timeout in Guzzle ist 0, was bedeutet, dass unendlich lange gewartet wird. Bei einem privaten Proxy ist dies eine schlechte Standardeinstellung, da ein langsames Ausgangsgerät einen Worker auf unbestimmte Zeit blockieren kann. Legen Sie zwei Werte fest: connect_timeout begrenzt die TCP-Verbindung zum Proxy und wird nur vom cURL-Handler unterstützt; timeout begrenzt die gesamte Übertragung in Sekunden.
$client = new GuzzleHttp\Client([
'proxy' => $proxyUrl,
'connect_timeout' => 10, // seconds to reach the proxy
'timeout' => 45, // total seconds for the request
]);
Privathaushalts-Exits sind langsamer als solche in Rechenzentren, daher sollten Sie für das Gesamt-Timeout mehr Zeit einplanen als bei einer direkten Anfrage. Ein regelmäßig auftretendes Verbindungs-Timeout deutet eher auf ein Problem mit dem Netzwerkpfad hin als auf ein Problem mit dem Ziel.
Umgang mit Ausnahmen eines Guzzle-Proxys
Guzzle löst typisierte Ausnahmen aus, die alle von der Klasse GuzzleHttp\Exception\RequestException abgeleitet sind. Anhand der Art der Ausnahme lässt sich erkennen, wo der Fehler aufgetreten ist.
| Ausnahme | Wann sie ausgelöst wird | Typische Ursache im Proxy |
|---|
ConnectException | Überhaupt keine HTTP-Antwort | Proxy-Host nicht erreichbar, falscher Port, Zeitüberschreitung bei der Verbindung, DNS-Fehler |
ClientException | 4xx-Antwort | 407 – falsche Anmeldedaten; 403/429 vom Zielserver |
ServerException | 5xx-Antwort | Fehler beim Zielserver oder der Proxy konnte den Zielserver nicht erreichen |
RequestException | Alles andere | Totaler Timeout nach den Headern, TLS-Fehler |
Ein 407-Fehler sollte genauer untersucht werden. Es ist der Proxy, nicht das Ziel, der die Anfrage ablehnt, sodass ein erneuter Versuch erst dann hilft, wenn die Anmeldedaten geändert werden.
<?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.
}
Ausnahmen mit http_errors deaktivieren
Wenn Sie lieber anhand des Statuscodes verzweigen möchten, anstatt Ausnahmen abzufangen, setzen Sie http_errors => false. Guzzle gibt dann bei jedem Status, einschließlich 407, die Antwort zurück, und Sie überprüfen getStatusCode() selbst. ConnectException wird weiterhin ausgelöst, da keine Antwort zurückgegeben werden kann.
$response = $client->get('https://example.com/', ['http_errors' => false]);
if ($response->getStatusCode() === 407) {
// handle auth failure without an exception
}
Diagnose: on_stats und debug
Wenn eine Guzzle-Proxy-Anfrage langsam ist oder zeitweise fehlschlägt, gibt es zwei Optionen, mit denen Sie feststellen können, was auf der Leitung passiert ist. on_stats empfängt nach jeder Übertragung ein GuzzleHttp\TransferStats-Objekt mit der Gesamtzeit und den cURL-Handler-Statistiken wie Namensauflösung, Verbindungsaufbau und TLS-Zeiten.
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 aktiviert CURLOPT_VERBOSE und gibt den CONNECT-Austausch aus, einschließlich der Antwortzeile des Proxys. Verwenden Sie es einmal, um zu überprüfen, ob der Tunnel aufgebaut ist, und entfernen Sie es anschließend wieder: Es schreibt Anmeldedaten in STDOUT. Durch die Übergabe eines fopen()-Streams anstelle von true wird die Ausgabe in eine Datei umgeleitet.
Sticky-Sessions auf Port 10000
Port 9000 wechselt die Ausgangs-IP bei jeder Anfrage. Für mehrstufige Abläufe (Anmeldung, dann Seiten abrufen, dann paginieren) benötigen Sie dieselbe IP-Adresse, was Port 10000 gewährleistet. Die Sitzung wird über den Benutzernamen ausgewählt, nicht über einen Header: Fügen Sie an „-session-XXXXXXXX“ eine 8-stellige alphanumerische ID an und optional „-lifetime-SECONDS“ im Bereich von 180 bis 86400. Ziel-Suffixe wie „-country-us“ werden in dieselbe Zeichenfolge eingefügt.
<?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,
]);
Jede Anfrage von $client nutzt nun zehn Minuten lang dieselbe Ausgangs-IP. Erstellen Sie einen neuen Client (oder übergeben Sie eine neue proxy-Option), wenn Sie eine andere IP wünschen.
Überprüfung der Ausgangs-IP
Bevor Sie einer Sitzung vertrauen, überprüfen Sie, was das Ziel sieht. Rufen Sie einen IP-Echo-Dienst zweimal über denselben Client auf: Bei einer „Sticky Session“ geben beide Aufrufe dieselbe Adresse zurück, bei Port 9000 unterscheiden sie sich.
$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');
Ein kleiner Helfer für Wiederholungsversuche, der die Sitzung wechselt
Privathaushalte können mitten in einer Sitzung die Verbindung unterbrechen, und Zielserver blockieren manchmal einzelne IP-Adressen. Ein pragmatischer Ansatz besteht darin, eine fehlgeschlagene Anfrage mit einer neuen Sitzungs-ID erneut zu versuchen. Dadurch wird sowohl die IP-Adresse des Exits geändert als auch die Wiederverwendung einer Verbindung vermieden, die sich bereits in einem fehlerhaften Zustand befindet. Die unten stehende Hilfsfunktion baut den Client bei jedem Versuch neu auf und führt nur bei Verbindungsfehlern, Timeouts und Statuscodes, die auf eine Sperrung hindeuten, einen erneuten Versuch durch.
<?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);
}
Der gleiche Code funktioniert auch mit Port 9000, wenn man das Sitzungssuffix weglässt, da bei jedem Versuch ohnehin eine neue IP-Adresse zugewiesen wird. Halte die Anzahl der Versuche gering: Ein Ziel, das dreimal hintereinander einen 403-Fehler zurückgibt, gibt Aufschluss über das „Request Fingerprinting“, nicht über den Proxy.
Häufige Fehler
Verwendung eines https://-Schemas in der Proxy-URL
Der Proxy-Hop erfolgt über einfaches HTTP; das HTTPS des Ziels läuft innerhalb des CONNECT-Tunnels. Beide Array-Schlüssel sollten eine http://-Proxy-URL enthalten. Das typische Symptom ist ein ConnectException, der einen SSL-Handshake mit dem Proxy-Host erwähnt.
Das Vergessen der no-Liste nach dem Festlegen von proxy
Sobald Sie proxy übergeben haben, wird NO_PROXY ignoriert. Interne Zustandsprüfungen, Metadaten-Endpunkte und Datenbanken, die über HTTP erreicht werden, durchlaufen den Residential-Proxy und schlagen fehl oder – schlimmer noch – es kommt zu Datenlecks. Übergeben Sie no immer explizit.
timeout auf Null belassen
Guzzle wartet standardmäßig unbegrenzt lange. Eine einzige blockierte Residential-Verbindung kann einen Queue-Worker blockieren, bis ein Supervisor ihn beendet. Setzen Sie sowohl connect_timeout als auch timeout für jeden über den Proxy verbundenen Client.
Wiederholung eines 407-Fehlers
Ein 407-Fehler bedeutet, dass der Proxy die Anmeldedaten abgelehnt hat. Ein erneuter Versuch mit demselben Benutzernamen und Passwort verschwendet nur Zeit. Brechen Sie den Vorgang frühzeitig ab, protokollieren Sie den Benutzernamen (niemals das Passwort) und überprüfen Sie die Syntax-Suffixe.
Nächster Schritt
Die oben beschriebene Konfiguration funktioniert unverändert mit den Residential-Proxys Geonode: 2,5 Millionen IP-Adressen in über 195 Ländern, HTTP, HTTPS und SOCKS5 auf demselben Host sowie Sticky-Sessions von bis zu 24 Stunden, die vollständig über den Benutzernamen ausgewählt werden. Das Targeting nach Land, Bundesland, Stadt und ASN erfolgt nach derselben hier gezeigten Syntax, sodass keine Codeänderungen erforderlich sind, wenn Sie den Ausgangsstandort eingrenzen. Die Tarife sind pay-as-you-go ohne Vertragsbindung, beginnend bei 10 GB für 7,92 $ pro Monat, und eine 3-tägige Testversion kostet 5 $. Alle Details finden Sie unter Privathaushalts-Proxys oder vergleichen Sie die Tarife auf der Preisseite. Für Scraping-Szenarien, die über die Proxy-Einrichtung hinausgehen, deckt der Anwendungsfall „Web-Scraping“ den Rest der Pipeline ab.
FAQ
Wie richte ich einen Guzzle-Proxy für jede Anfrage eines bestimmten Clients ein?
Übergeben Sie die Proxy-Option im Client-Konstruktor statt pro Anfrage. new Client(['proxy' => 'http://user:pass@host:port']) wendet den Proxy auf jede Anfrage an, die der Client sendet, und jede Proxy-Option auf Anfrageebene überschreibt diese Einstellung. Dies ist das übliche Vorgehen, wenn der gesamte Datenverkehr eines Workers über denselben Provider laufen soll.
Warum funktioniert mein Guzzle-Proxy für URLs vom Typ „http://“, versagt aber bei Zielen vom Typ „https://“?
In der Regel fehlt der Schlüssel „https“ in der Array-Form oder er verweist auf ein „https://“-Schema. Guzzle leitet HTTPS-Ziele über einen HTTP-Proxy mit CONNECT weiter, daher sollte die Proxy-URL für den Schlüssel „https“ weiterhin mit „http://“ beginnen. Überprüfen Sie das Array-Formular und stellen Sie sicher, dass der Proxy-Port CONNECT akzeptiert, was bei den „Geonode“-Ports der Fall ist.
Was bedeutet ein 407 „Proxy Authentication Required“ in Guzzle?
Der Proxy hat Ihre Anmeldedaten abgelehnt, bevor er Daten weitergeleitet hat. Guzzle löst dies als ClientException mit dem Status 407 aus, da es sich um eine 4xx-Antwort handelt. Überprüfen Sie die Schreibweise des Benutzernamens, das Passwort und stellen Sie sicher, dass Sonderzeichen im Passwort in der URL prozentkodiert sind. Wenn http_errors auf „false“ gesetzt ist, erhalten Sie das 407-Antwortobjekt anstelle einer Ausnahme.
Berücksichtigt Guzzle die Umgebungsvariablen „HTTP_PROXY“ und „NO_PROXY“?
Ja, wenn Sie keine Proxy-Option übergeben, liest Guzzle die Werte „HTTP_PROXY“, „HTTPS_PROXY“ und „NO_PROXY“ aus der Umgebung aus. Sobald Sie selbst eine Proxy-Option übergeben, wird die automatische Verarbeitung von NO_PROXY beendet, sodass Sie den Schlüssel no explizit übergeben müssen, typischerweise explode(',', getenv('NO_PROXY')).
Kann ich über mehrere Guzzle-Anfragen hinweg dieselbe Ausgangs-IP beibehalten?
Ja, verwenden Sie eine „Sticky Session“. Richten Sie den Guzzle-Proxy auf Port 10000 aus und fügen Sie -session-XXXXXXXX (8 alphanumerische Zeichen) sowie ein optionales -lifetime-SECONDS-Suffix zum Benutzernamen hinzu. Jede Anfrage, die diesen Benutzernamen wiederverwendet, nutzt für die von Ihnen festgelegte Laufzeit – zwischen 180 Sekunden und 24 Stunden – dieselbe Ausgangs-IP-Adresse.
Gibt es Einschränkungen, die ich beim Scraping über einen Guzzle-Proxy berücksichtigen sollte?
Die Bandbreite für Privatkunden wird pro Gigabyte abgerechnet, daher summieren sich große HTML-Seiten und Bilder. Sticky-Sessions dauern maximal 24 Stunden und können vorzeitig enden, wenn die Verbindung zum Upstream-Gerät unterbrochen wird; daher muss der Code eine geänderte Ausgangs-IP tolerieren. Beachten Sie außerdem die Nutzungsbedingungen, Robots-Regeln und Ratenbeschränkungen der Zielwebsite; ein Proxy ändert nichts an Ihren rechtlichen Verpflichtungen.