httpx ist der HTTP-Client, auf den die meisten neuen Python-Projekte zurückgreifen, wenn sie über die Möglichkeiten von requests hinauswachsen – vor allem, weil er eine einheitliche API für synchronen und asynchronen Code bietet. Diese Anleitung zeigt, wie man einen httpx-Proxy sowohl in Client als auch in AsyncClient einbindet, sich über einen rotierenden Endpunkt für Privatanwender authentifiziert, eine „Sticky Session“ festlegt, SOCKS5 hinzufügt, Timeouts und Wiederholungsversuche optimiert und die drei Fehler diagnostiziert, die tatsächlich auftreten können. Am Ende verfügen Sie über einen wiederverwendbaren Client, der über einen Proxy leitet und seine Ausgangs-IP nachweisen kann.
Die Beispiele beziehen sich auf httpx 0.27 oder neuer. Wenn Sie eine ältere Version verwenden, beachten Sie bitte, dass das Schlüsselwort „proxies=“ in httpx 0.26 in „proxy=“ und „mounts=“ umbenannt wurde; im Folgenden werden durchgehend die aktuellen Bezeichnungen verwendet.
Zwei Möglichkeiten zur Konfiguration eines httpx-Proxys: proxy und mounts
httpx bietet Ihnen eine einfache und eine präzise Konfigurationsmöglichkeit. proxy= nimmt eine einzelne URL entgegen und leitet jede Anfrage darüber weiter. mounts= ordnet URL-Muster den Transporten zu, wobei jeder Transport seinen eigenen Proxy, seine eigene Anzahl an Wiederholungsversuchen und seine eigenen Verbindungslimits haben kann.
| Option | Geltungsbereich | Anwendungsfall |
|---|
proxy="http://..." | Gesamter Client, alle Schemata | Ein Proxy für alles – dies ist der übliche Fall bei einem rotierenden Gateway |
mounts={"https://": HTTPTransport(proxy=...)} | Pro URL-Muster | Unterschiedliche Proxys pro Schema oder Host oder Umgehung des Proxys für interne Dienste |
httpx.get(url, proxy=...) | Einzelner Aufruf auf oberster Ebene | Schnelle Überprüfungen und Skripte ohne langlebigen Client |
Die einfache Form sieht wie folgt aus:
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
with httpx.Client(proxy=PROXY, timeout=20.0) as client:
r = client.get("https://api.ipify.org?format=json")
print(r.status_code, r.json())
Port 9000 ist das rotierende HTTP(S)-Gateway, sodass jede neue Verbindung eine andere private IP-Adresse erhält. Die Anmeldedaten befinden sich in der URL als Basic-Auth, und httpx wandelt sie ohne zusätzlichen Code in einen Proxy-Authorization-Header um.
Die Tücken des schemaspezifischen Routings
Mit mounts können Sie den Zielen http:// und https:// separate Transportwege zuweisen. Die Tücke liegt im Schema der Proxy-URL selbst. Für ein Ziel „https://“ sendet httpx „CONNECT“ an den Proxy und handelt dann innerhalb dieses Tunnels TLS mit dem Ursprungsserver aus. Der Hop zum Proxy erfolgt über reines HTTP, daher sollte die Proxy-URL auf dem https://-Mount normalerweise mit http:// beginnen, nicht mit https://. Wenn man dort „https://“ eingibt, weist man httpx an, den Proxy-Sprung selbst in TLS einzubinden, was die meisten Gateways nicht unterstützen, und es kommt zu einem Handshake-Fehler, der scheinbar nichts mit Proxys zu tun hat.
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
mounts = {
"http://": httpx.HTTPTransport(proxy=PROXY),
"https://": httpx.HTTPTransport(proxy=PROXY), # http:// scheme is correct here
# internal services stay direct: mount a transport without a proxy
"all://*.internal.example": httpx.HTTPTransport(),
}
with httpx.Client(mounts=mounts) as client:
print(client.get("https://api.ipify.org?format=json").json())
Spezifischere Muster haben Vorrang vor allgemeinen, daher umgeht der Eintrag „all://*.internal.example“ den Proxy für diese Domain, während alles andere weitergeleitet wird.
Authentifizierung in der URL
Die Anmeldedaten für Geonode bestehen aus einem einfachen Benutzernamen und einem Passwort, daher benötigen Sie lediglich das Benutzerinformationsformular http://user:pass@host:port. Zwei praktische Hinweise: Erstens: Wenn das Passwort Zeichen wie @, : oder / enthält, führen Sie vor dem Erstellen der URL eine Prozent-Kodierung mit urllib.parse.quote durch. Zweitens: Halten Sie geheime Daten aus der Versionskontrolle fern, indem Sie sie aus der Umgebung einlesen:
import os
from urllib.parse import quote
import httpx
user = os.environ["GEONODE_USER"]
password = quote(os.environ["GEONODE_PASS"], safe="")
proxy = f"http://{user}:{password}@premium-residential.geonode.com:9000"
client = httpx.Client(proxy=proxy)
Alles, was folgt, fügt Targeting- und Sitzungsoptionen hinzu, indem der Benutzername erweitert wird, nicht die URL, sodass das gleiche Muster weiterhin funktioniert.
Synchronisation von Client und AsyncClient
Der asynchrone Client verwendet dasselbe Schlüsselwort proxy=. Der einzige Unterschied tritt auf, wenn Sie zu mounts wechseln: AsyncClient erfordert httpx.AsyncHTTPTransport, nicht httpx.HTTPTransport. Die Übergabe des synchronen Transports an einen asynchronen Client löst bei der ersten Anfrage einen Fehler aus.
import asyncio
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
async def fetch(client: httpx.AsyncClient, url: str) -> str:
r = await client.get(url)
r.raise_for_status()
return r.json()["ip"]
async def main() -> None:
transport = httpx.AsyncHTTPTransport(proxy=PROXY, retries=2)
async with httpx.AsyncClient(
mounts={"all://": transport}, timeout=httpx.Timeout(10.0, connect=5.0)
) as client:
urls = ["https://api.ipify.org?format=json"] * 5
ips = await asyncio.gather(*(fetch(client, u) for u in urls))
print(ips)
asyncio.run(main())
Da Port 9000 pro Verbindung wechselt, werden bei den fünf gleichzeitigen Anfragen in der Regel mehrere unterschiedliche IP-Adressen ausgegeben. Dies ist das erwartete Verhalten bei einem rotierenden Pool; wenn die Adressen übereinstimmen sollen, verwenden Sie eine „Sticky Session“ (siehe unten).
SOCKS5 bis httpx[socks]
Die SOCKS-Unterstützung in einer httpx-Proxy-Konfiguration ist optional. Installieren Sie sie mit pip install "httpx[socks]", wodurch das Paket socksio hinzugefügt wird, und tauschen Sie anschließend das Schema aus. Der rotierende Port von Geonode (SOCKS5) ist 11000:
import httpx
PROXY = "socks5://USERNAME:PASSWORD@premium-residential.geonode.com:11000"
with httpx.Client(proxy=PROXY) as client:
print(client.get("https://api.ipify.org?format=json").json())
Ohne die zusätzliche Installation löst httpx einen ImportError-Fehler aus, der socksio erwähnt, sobald es eine socks5://-Proxy-URL erkennt. Für gewöhnliches Scraping lassen sich die HTTP-Ports einfacher debuggen, da die 407-Antworten lesbar sind.
Umgebungsvariablen und trust_env
Mit dem Standardwert trust_env=True liest httpx die Werte HTTP_PROXY, HTTPS_PROXY, ALL_PROXY und NO_PROXY und wendet sie automatisch an. Das ist auf einer Workstation praktisch, führt jedoch in CI zu Verwirrung, da ein Unternehmensproxy in der Umgebung Ihr Routing unbemerkt umschreibt. Wenn Sie den Proxy im Code festlegen, sollten Sie erwägen, die Umgebungsabfrage zu deaktivieren, damit das Verhalten überall identisch ist:
import httpx
client = httpx.Client(
proxy="http://USERNAME:PASSWORD@premium-residential.geonode.com:9000",
trust_env=False,
)
Ein explizites proxy= hat bereits Vorrang vor der Umgebung; trust_env=False verhindert zudem, dass httpx SSL_CERT_FILE ausliest. Legen Sie es daher bewusst fest.
Timeouts und Wiederholungsversuche auf Transportebene
Ein Proxy fügt einen zusätzlichen Schritt hinzu, wodurch sich die Verbindungszeit verlängert und das standardmäßige Timeout von 5 Sekunden für private Internetanschlüsse manchmal zu knapp bemessen ist. Mit httpx.Timeout können Sie ein Gesamtbudget mit einem separaten Verbindungslimit festlegen, und HTTPTransport(retries=...) wiederholt fehlgeschlagene Verbindungsversuche, noch bevor die Anfrage überhaupt gesendet wird.
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
transport = httpx.HTTPTransport(proxy=PROXY, retries=2)
timeout = httpx.Timeout(15.0, connect=8.0)
with httpx.Client(mounts={"all://": transport}, timeout=timeout) as client:
r = client.get("https://api.ipify.org?format=json")
print(r.json())
Wiederholungsversuche auf Transportebene decken ConnectError und ähnliche Fehler beim Aufbau der Verbindung ab. Sie führen keine Wiederholungsversuche bei bestimmten Statuscodes durch und auch nicht, nachdem Bytes gesendet wurden; daher erreicht ein 502 vom Gateway oder ein 429 der Zielseite dennoch Ihren Code. Behandeln Sie diese in einer eigenen kleinen Schleife, in der Sie auch entscheiden können, ob die Sitzungs-ID geändert werden soll.
Sticky-Sessions mit der Benutzernamen-Grammatik „Geonode“
Bei mehrstufigen Abläufen wie Anmeldung, Paginierung und Download ist eine Rotation bei jeder Verbindung nicht sinnvoll. In diesen Fällen muss Ihr httpx-Proxy über alle Anfragen hinweg dieselbe Ausgangs-IP beibehalten. Geonode kodiert dies im Benutzernamen und stellt ihn auf dem HTTP(S)-Sticky-Port 10000 bereit:
USER[-type-residential|datacenter|mix][-country-xx][-state-name|-city-name][-asn-N][-session-XXXXXXXX][-lifetime-SECONDS]
Die Sitzungs-ID besteht aus 8 alphanumerischen Zeichen, die Sie generieren; die Lebensdauer beträgt 180 bis 86.400 Sekunden (24 Stunden). Bundesland und Stadt dürfen nicht kombiniert werden, und asn erfordert die Angabe eines Landes.
import secrets
import httpx
def sticky_client(country: str = "us", lifetime: int = 600) -> httpx.Client:
session_id = secrets.token_hex(4) # 8 hex characters
username = f"USERNAME-country-{country}-session-{session_id}-lifetime-{lifetime}"
proxy = f"http://{username}:PASSWORD@premium-residential.geonode.com:10000"
return httpx.Client(proxy=proxy, timeout=httpx.Timeout(15.0, connect=8.0))
with sticky_client() as client:
first = client.get("https://api.ipify.org?format=json").json()["ip"]
second = client.get("https://api.ipify.org?format=json").json()["ip"]
print(first == second, first)
Ein neues „session_id“ bedeutet eine neue IP-Adresse, daher bedeutet die „manuelle“ Rotation lediglich die Erstellung eines neuen Clients. Halten Sie die Lebensdauer der Sticky-Ports so kurz wie für den Auftrag erforderlich.
Überprüfen Sie die Ausgangs-IP
Jedes der oben genannten Beispiele greift auf https://api.ipify.org?format=json zu, das für jede Adresse, von der aus der Aufruf erfolgt, {"ip": "..."} zurückgibt. Vergleichen Sie dies einmal mit einem direkten Aufruf (ohne Proxy), und Sie haben einen zweizeiligen Smoke-Test, der belegt, dass sich der Proxy im Pfad befindet. Führen Sie dies durch, bevor Sie irgendetwas anderes debuggen: Viele Meldungen wie „Der httpx-Proxy funktioniert nicht“ beziehen sich auf Anfragen, bei denen der Proxy gar nicht verwendet wurde, weil eine Umgebungsvariable oder ein Mount-Muster die Steuerung übernommen hat.
Umgang mit den Fehlern 407 ProxyError und ConnectError
Drei Fehler sind für fast alle Probleme mit dem httpx-Proxy verantwortlich. In der Tabelle sind die jeweiligen Fehler, ihre Ursachen und die entsprechenden Lösungen aufgeführt.
| Symptom | Ausnahme | Übliche Ursache | Lösung |
|---|
| Antwortstatus 407 | httpx.ProxyError (wird beim Aufbau des Tunnels für HTTPS-Ziele ausgelöst) oder eine einfache 407-Antwort für HTTP-Ziele | Falsche Anmeldedaten oder eine Benutzernamenoption, die das Gateway ablehnt | Überprüfen Sie die Syntax des Benutzernamens; führen Sie eine Prozent-Kodierung des Passworts durch |
| Tunnel abgelehnt, 502/503 vom Proxy | httpx.ProxyError | Das Gateway konnte den Tunnel nicht öffnen oder der Port ist für das Protokoll falsch | Überprüfen Sie 9000/10000 für HTTP(S), 11000/12000 für SOCKS5 |
| Proxy-Host nicht erreichbar | httpx.ConnectError | DNS, Firewall oder falscher Hostname | Host manuell auflösen; Timeout für connect erhöhen |
| Anfrage hängt und schlägt dann fehl | httpx.ConnectTimeout / httpx.ReadTimeout | Langsame private Internetverbindung oder blockiertes Ziel | Timeout erhöhen, mit einer neuen Sitzungs-ID erneut versuchen |
import httpx
PROXY = "http://USERNAME:PASSWORD@premium-residential.geonode.com:9000"
def get_via_proxy(url: str) -> httpx.Response | None:
try:
with httpx.Client(proxy=PROXY, timeout=httpx.Timeout(15.0, connect=8.0)) as client:
r = client.get(url)
if r.status_code == 407:
raise RuntimeError("proxy rejected credentials (407)")
return r
except httpx.ProxyError as e:
print("proxy refused tunnel:", e)
except httpx.ConnectError as e:
print("cannot connect to proxy host:", e)
except httpx.TimeoutException as e:
print("timed out:", type(e).__name__)
return None
resp = get_via_proxy("https://api.ipify.org?format=json")
print(resp.json() if resp else "failed")
Alle httpx-Ausnahmen leiten sich von httpx.HTTPError ab, daher reicht ein einziger allgemeiner except für die Protokollierung aus; die Unterscheidung zwischen ProxyError und ConnectError gibt jedoch Aufschluss darüber, ob die Anmeldedaten oder das Netzwerk überprüft werden müssen.
Häufige Fehler
Verwendung von „https://“ in der Proxy-URL
Wie oben erläutert, beschreibt das Proxy-URL-Schema den Hop zum Proxy. Verwenden Sie „http://“ für die HTTP(S)-Ports, auch wenn das Ziel „https://“ lautet. Reservieren Sie „https://“ für Gateways, die dem Proxy explizit TLS anbieten.
Einbinden von „HTTPTransport“ in ein „AsyncClient“
mounts auf AsyncClient muss AsyncHTTPTransport verwenden. Der Sync-Transport wird bei der Erstellung akzeptiert, schlägt jedoch zum Zeitpunkt der Anfrage mit einem wenig aussagekräftigen Fehler fehl, sodass dieser Fehler in Tests, die niemals eine Anfrage senden, leicht übersehen wird.
Erwartung einer IP-Adresse vom rotierenden Port
Port 9000 wechselt pro Verbindung. Wenn zwei Anfragen hintereinander dieselbe IP-Adresse nutzen müssen, verwenden Sie Port 10000 mit einer -session--ID im Benutzernamen. Die Wiederverwendung eines Client fixiert die IP-Adresse nicht automatisch, da Verbindungen im Pool ersetzt werden können.
Die Umgebung hat Vorrang vor Ihrem Code
Ein vereinzeltes „HTTPS_PROXY“ in einem Docker-Image oder CI-Runner leitet den Datenverkehr um, den Sie für direkt gehalten haben, und „NO_PROXY“ kann Hosts ausschließen, von denen Sie erwartet haben, dass sie über den Proxy geleitet werden. Setzen Sie „trust_env=False“, wenn das Proxy-Routing deterministisch sein muss.
Nächster Schritt
Sobald der oben genannte Client eine Privat-IP zurückgibt und eine „Sticky Session“ über mehrere Anfragen hinweg bestehen bleibt, ist die einzige verbleibende Variable der dahinterstehende Pool. Die Privat-Proxys von Geonode stellen die in dieser Anleitung verwendeten Endpunkte bereit: 2,5 Millionen IP-Adressen online, 30–50 Millionen eindeutige IP-Adressen, die monatlich in über 195 Ländern rotieren, mit Targeting nach Land, Bundesland, Stadt und ASN über die Benutzernamen-Syntax sowie „sticky sessions“ von bis zu 24 Stunden. Die Preise beginnen bei 0,27 $/GB bei großen Volumina, das Einstiegspaket umfasst 10 GB für 7,92 $/Monat und eine 3-tägige Testversion kostet 5 $. Wenn Sie darauf aufbauend einen Crawler entwickeln möchten, beschreibt der Anwendungsbeispiel zum Web-Scraping beschreibt, wie man Proxys mit einer Wiederholungslogik kombiniert, und rotierende Rechenzentrums-Proxys sind die kostengünstigere Option für Ziele, die Rechenzentrums-IP-Bereiche nicht blockieren.
FAQ
Wie richte ich einen httpx-Proxy für alle Anfragen ein?
Übergeben Sie die Proxy-URL an den Client-Konstruktor: httpx.Client(proxy="http://user:pass@host:port"). Jede über diesen Client gestellte Anfrage, sowohl für das Ziel http:// als auch für das Ziel https://, wird über den Proxy geleitet. Das gleiche Schlüsselwort funktioniert bei httpx.AsyncClient und bei Helfern der obersten Ebene wie httpx.get(). Anmeldedaten in der URL werden automatisch als Proxy-Authorization gesendet.
Was ist der Unterschied zwischen Proxy und Mounts in httpx?
proxy ist eine Abkürzung, die alles über einen einzigen Proxy leitet. mounts ordnet URL-Muster wie "http://" oder "https://" Transporten zu, von denen jeder seinen eigenen Proxy, eigene Wiederholungsversuche und eigene Limits haben kann. Verwenden Sie mounts, wenn Sie je nach Schema oder Host unterschiedliche Proxys benötigen oder den Proxy für bestimmte Ziele umgehen möchten, indem Sie einen Transport ohne Proxy einbinden.
Warum schlägt die Einbindung von https:// fehl, wenn die Proxy-URL mit https://? beginnt?
Das Schema in der Proxy-URL beschreibt, wie httpx mit dem Proxy kommuniziert, nicht mit dem Ziel. Bei Zielen vom Typ „https://“ eröffnet httpx einen CONNECT-Tunnel über eine einfache HTTP-Verbindung zum Proxy; daher sollte die Proxy-URL normalerweise „http://“ verwenden. Die Verwendung von „https://“ weist httpx an, TLS mit dem Proxy selbst auszuhandeln, was die meisten Proxy-Endpunkte jedoch nicht unterstützen.
Unterstützt httpx SOCKS5-Proxys?
Ja, jedoch erst nach der Installation der optionalen Abhängigkeit mit pip install httpx[socks], die socksio einbindet. Übergeben Sie dann proxy="socks5://user:pass@host:port" genau so, wie Sie es bei einem HTTP-Proxy tun würden. Bei Geonode ist der rotierende Port von SOCKS5 11000 und der Sticky-Port von SOCKS5 12000; HTTP(S)-Ports akzeptieren keine SOCKS-Handshakes.
Kann httpx eine Anfrage automatisch wiederholen, wenn die Proxy-Verbindung fehlschlägt?
Nur auf Transportebene. httpx.HTTPTransport(retries=2) wiederholt fehlgeschlagene Verbindungsversuche, einschließlich Verbindungsfehlern zum Proxy, wiederholt jedoch niemals eine Anfrage, die bereits eine Antwort wie 407 oder 502 erhalten hat. Wiederholungsversuche bei bestimmten HTTP-Statuscodes sollten in Ihrer eigenen Schleife erfolgen, in der Sie die Sitzungs-ID wechseln oder bewusst eine Wartezeit einlegen können.
Gibt es Einschränkungen hinsichtlich dessen, was eine httpx-Proxy-Konfiguration mit privaten IP-Adressen tun kann?
Ein Proxy ändert Ihre Ausgangs-IP; er ändert nicht, was eine Website zulässt. Halten Sie sich an robots.txt, die Nutzungsbedingungen und Ratenbeschränkungen, halten Sie die Anzahl gleichzeitiger Zugriffe moderat und vermeiden Sie das Sammeln personenbezogener Daten, für deren Verarbeitung Sie keine Grundlage haben. Privatsitzungen sind zudem auf 24 Stunden begrenzt; danach wird die IP freigegeben und Sie müssen eine neue Sitzung eröffnen.