0%

Mit einer API kommunizieren · Übung

Jede Wartezeit begrenzen

Dieser Fehlerfall überrascht viele beim ersten Mal: Ein Programm stürzt nicht ab, meldet keinen Fehler und wird nicht fertig. Es bleibt einfach hängen. Etwas am anderen Ende des Netzwerks antwortet nicht mehr, und nichts hat deinem Code gesagt, dass er aufhören soll zu warten.

Ein Timeout ist die Anweisung, das Warten zu beenden. Er ist keine Option für Fortgeschrittene, zu der du erst nach einer schlechten Erfahrung greifst. Er gehört zu jeder Anfrage, die du schreibst.

Zwei Wartezeiten statt einer

Requests erwartet ein Paar, dessen beide Teile unterschiedliche Bedeutungen haben:

DEFAULT_TIMEOUT = (1.0, 1.0)

Die erste Zahl begrenzt die Verbindungsphase: wie lange du wartest, bis der Dienst überhaupt abnimmt. Die zweite begrenzt die Lesephase: wie lange du zwischen Teilen der Antwort wartest, sobald das Gespräch begonnen hat.

Die Trennung ist nützlich, weil die beiden Fehler unterschiedliche Bedeutungen haben. Ein Dienst, der die Verbindung nie annimmt, ist wahrscheinlich ausgefallen oder nicht erreichbar. Ein Dienst, der annimmt und dann verstummt, läuft wahrscheinlich, hat aber Schwierigkeiten.

Beobachte, wie dieselbe Verzögerung zwei verschiedene Ergebnisse erzeugt

Die Übungs-API hat eine Route, die sich absichtlich Zeit lässt. Fordere eine Wartezeit von 250 Millisekunden an, mit einer Lesegrenze von einer ganzen Sekunde:

request_json(
    "/scenarios/slow",
    params={"delay_ms": 250},
    timeout=(1.0, 1.0),
)

Die Anfrage gelingt. Die Verzögerung bleibt innerhalb der Grenze, deshalb passiert nichts Ungewöhnliches.

Die zweite Route ist /scenarios/timeout mit params={"delay_ms": 750}, wie in API_CONTRACT.md festgehalten. Führe sie mit jedem der folgenden Timeouts einmal aus. Die Ausnahme zeigt, welche Seite zuerst aufhört zu warten:

import requests
from api_catalog import request_json

for timeout in [(1.0, 1.5), (1.0, 0.2)]:
    try:
        request_json(
            "/scenarios/timeout",
            params={"delay_ms": 750},
            timeout=timeout,
        )
    except requests.HTTPError as exc:
        print("HTTP response:", exc.response.status_code)
    except requests.ReadTimeout:
        print("Client stopped waiting; no response arrived.")

Das sind zwei getrennte Versuche, keine Wiederholungsstrategie. Behalte die Ausnahmebehandlung in diesem Untersuchungscode; request_json fängt weiterhin nichts ab.

Mit der großzügigen Lesegrenze (1.0, 1.5) kommt der Server weit genug, um seine eigene Antwort zu senden: eine absichtliche HTTP-504. Das ist eine Antwort. Sie ist angekommen. raise_for_status() macht daraus einen HTTPError, und du kannst exc.response.status_code untersuchen und dort 504 ablesen.

Mit der knappen Lesegrenze (1.0, 0.2) gibt dein Client zuerst auf. Requests löst requests.ReadTimeout aus. Es gibt kein Antwortobjekt, keinen Statuscode, nichts zu untersuchen. Dein Programm hat aufgehört zuzuhören, bevor etwas zurückkam.

Derselbe langsame Dienst, zwei unterschiedliche Ereignisse:

Was passiert istAusnahmeGibt es einen Status?
Der Server antwortete langsam mit einem Fehlerrequests.HTTPErrorJa, 504
Der Client hörte zuerst auf zu wartenrequests.ReadTimeoutNein

Der Unterschied ist wichtig, weil die beiden Fälle unterschiedliche nächste Schritte nahelegen. In diesem kontrollierten Szenario bedeutet eine 504, dass eine HTTP-Fehlerantwort von der Serverseite angekommen ist. Ein ReadTimeout bedeutet, dass der Client zuerst aufgehört hat zu warten, und sagt nichts Sicheres darüber aus, was der Server schließlich getan hat.

Rate nicht anhand der Uhr

Für request_json gelten zwei Regeln, und beide verlangen Zurückhaltung.

Fange hier nichts ab. Diese Funktion sendet eine Anfrage und gibt einen dekodierten Inhalt zurück. Sie weiß nicht, ob ihr Aufrufer eine Meldung ausgeben, mit einem Status enden oder auf einen Cache zurückgreifen möchte. Das hier zu entscheiden würde jedem künftigen Aufrufer diese Strategie aufzwingen.

Leite das Ergebnis an der Grenze nicht aus der verstrichenen Zeit ab. Es ist verlockend, die Dauer des Aufrufs zu messen und daraus rückwärts zu schließen. Die Ausnahmeklasse unterscheidet eine empfangene HTTP-Fehlerantwort von einem Timeout auf Clientseite. Die gemessene Zeit allein kann das auf einem langsamen Rechner oder an einem ausgelasteten Tag nicht zuverlässig leisten.

Reiche das Timeout-Tupel des Aufrufers unverändert weiter und lass die Ausnahme die Bedeutung übermitteln.

Was sagt dir ein ReadTimeout des Clients?

Warum sendet request_json bei jedem Aufruf einen Timeout statt nur bei bekanntermaßen langsamen Routen?

Aufgabe

Behalte DEFAULT_TIMEOUT genau als (1.0, 1.0) bei und stelle sicher, dass jede Anfrage ihren Timeout ausdrücklich weiterreicht. Reiche ein übergebenes Tupel (1.0, 1.5) oder (1.0, 0.2) unverändert weiter, bleibe bei genau einem Aufruf und lass HTTP- und Timeout-Ausnahmen nach außen gelangen.