Zurück zum Blog

Proxy gesetzt, trotzdem direkt: warum WebSockets an HTTPS_PROXY vorbeilaufen

Ein Container mit Pflicht-Proxy, eine Bibliothek mit trust_env=True – und trotzdem ging die Verbindung direkt ins Leere. Der Grund steckt in einem einzigen Wörterbuchzugriff: wss ist nicht https.

4 Min. Lesezeitvon Daniel AlischEngineering, KI-Agenten, DevOps

Seit kurzem dürfen meine Agenten-Container nicht mehr direkt ins Internet, jeder ausgehende Aufruf muss über einen Egress-Proxy. Das ist gewollt und für fast alles unauffällig. Eines Morgens fiel trotzdem die Sprachausgabe meines täglichen KI-Podcasts aus – mit einem Fehlerbild, das erst einmal nach „Proxy vergessen“ aussah und sich als etwas Feineres herausstellte.

Das Symptom: Verbindung abgelehnt, obwohl alles konfiguriert ist

Die Vertonung läuft über die Python-Bibliothek edge-tts, die Microsofts Sprachdienst per WebSocket anspricht. Der Lauf brach mit einem ClientConnectorError ab: Verbindungsaufbau zu einer öffentlichen IP auf Port 443 gescheitert. Das ist genau der Fehler, den man erwartet, wenn ein Programm am Proxy vorbei direkt ins Netz will.

Die Umgebung sah dabei einwandfrei aus. HTTPS_PROXY und https_proxy waren gesetzt, NO_PROXY nahm nur interne Adressen aus. Andere Werkzeuge im selben Container kamen problemlos raus. Die naheliegende Vermutung lautete: Die Bibliothek ignoriert Umgebungsvariablen einfach. Die Vermutung war bequem – und falsch.

Die erste Vermutung hält dem Code nicht stand

edge-tts baut seine Verbindung über aiohttp. Dort ist trust_env tatsächlich standardmäßig False, die Session liest Proxy-Variablen also nur, wenn man es ausdrücklich erlaubt. Nur: edge-tts erlaubt es. In der installierten Version 7.3.1 steht beim Aufbau der Session wörtlich trust_env=True. Die Bibliothek vertraut der Umgebung also – und verbindet trotzdem direkt.

Damit war klar, dass die Antwort eine Ebene tiefer liegt: in der Frage, welche Umgebungsvariable aiohttp für eine bestimmte Adresse eigentlich nachschlägt.

Die eigentliche Ursache: wss ist nicht https

In aiohttp (hier 3.14.5) übernimmt das die Hilfsfunktion get_env_proxy_for_url. Sie sammelt alle Proxy-Variablen ein und wählt dann den Eintrag, dessen Schlüssel dem Schema der Ziel-URL entspricht. Für eine Adresse mit https:// ist das der Eintrag aus HTTPS_PROXY. Ein WebSocket-Ziel beginnt aber mit wss:// – und sucht deshalb nach WSS_PROXY. Den hatte niemand gesetzt.

Das lässt sich in drei Zeilen nachstellen:

python
from yarl import URL
from aiohttp.helpers import get_env_proxy_for_url

for u in ["https://dienst.example.com/x", "wss://dienst.example.com/x"]:
    try:
        proxy, _ = get_env_proxy_for_url(URL(u))
        print(u, "->", proxy.host)
    except LookupError as e:
        print(u, "-> LookupError:", e)

Mit nur gesetztem HTTPS_PROXY liefert die erste Zeile den Proxy, die zweite einen LookupError. Der entscheidende Teil kommt danach: Die Session ruft diese Funktion innerhalb von suppress(LookupError) auf. Kein Proxy gefunden heißt also nicht „Fehler“, sondern „dann eben direkt“. Bei einem Netz mit Pflicht-Proxy führt dieser stille Rückfall zuverlässig in die abgelehnte Verbindung – und die Fehlermeldung zeigt nur noch die Folge, nicht die Ursache.

Zwei Wege, die tragen

Die Gegenprobe war schnell gemacht: Mit zusätzlich gesetztem WSS_PROXY findet dieselbe Funktion für die wss://-Adresse den Proxy. Das ist der eine Weg – die fehlende Variable in der Container-Umgebung ergänzen, dann greift er für jede Bibliothek, die so nachschlägt.

Der andere Weg ist der, den ich für den Podcast gewählt habe, weil er nicht von der Umgebung eines bestimmten Containers abhängt: den Proxy ausdrücklich übergeben. edge-tts bietet dafür einen Parameter an.

python
import os
import edge_tts

def env_proxy():
    # wss:// sucht WSS_PROXY, gesetzt ist aber nur HTTPS_PROXY
    return os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")

communicate = edge_tts.Communicate(text, voice=voice, proxy=env_proxy())

Ein expliziter proxy hat in aiohttp Vorrang vor allem, was aus der Umgebung kommt. Gibt es keinen Proxy, ist der Wert None und das Verhalten bleibt wie vorher. Nach dieser einen Zeile lief die Vertonung durch.

Was ich daraus mitnehme

Erstens: „Die Bibliothek ignoriert den Proxy“ ist eine Diagnose, die man am Quelltext prüfen sollte, bevor man sie aufschreibt. Meine erste Notiz zu dem Ausfall enthielt genau diesen Satz, und sie war ungenau. Die Bibliothek hat den Proxy nicht ignoriert, sie hat unter einem anderen Namen gesucht.

Zweitens: Wer einen Pflicht-Proxy einführt, sollte nicht nur HTTP-Aufrufe testen. WebSockets, gRPC und andere Protokolle haben eigene Schemata und damit eigene Nachschlage-Regeln. Ein kurzer Prüfaufruf je Protokoll im Container findet das, bevor es ein nächtlicher Job tut.

Drittens, allgemeiner: Ein stiller Rückfall auf „dann eben direkt“ ist bequem, solange das Netz offen ist. In dem Moment, in dem es geschlossen wird, verwandelt er eine klare Konfigurationslücke in einen Verbindungsfehler ohne Hinweis auf den Grund.

Fazit

Gesetzt war alles, gelesen wurde auch – nur nicht der richtige Schlüssel. Wer hinter einem Proxy mit WebSockets arbeitet, setzt WSS_PROXY mit oder übergibt den Proxy ausdrücklich. Und wer eine Fehlerursache aufschreibt, schaut vorher kurz in den Code. Das kostet fünf Minuten und spart die zweite, korrigierte Notiz.

Weiterlesen