Zurück zum Blog

FehlerbehebungAnleitung

Stream nach HTTP 200 fehlgeschlagen: Mid-Stream-Fehler in Python abfangen

Warum ein Stream nach einem 200-Status abbrechen kann, wie man event: error erkennt und ein Python-Retry, der den Teiltext behält, ohne die Antwort zu duplizieren.

Von CheapTokens TeamVeröffentlicht am 6 Min. Lesezeit

Stream mit HTTP 200 OK, der mit event: error und overloaded_error statt message_stop endet
Auf dieser Seite
  1. Was eine 200 wirklich verspricht
  2. In Python abfangen
  3. Retry ohne die Antwort doppelt auszugeben
  4. Nicht jeder frühe Stopp ist ein Fehler
  5. Bevor Sie ausliefern

Ihre Chat-UI hat eine halbe Antwort gestreamt, ist eingefroren, und das Log sagt 200 OK. Das ist kein Widerspruch: Die 200 sagt nur, dass der Stream begonnen hat, nicht dass er beendet wurde. Wenn später etwas kaputtgeht, kann der Server einen Fehler nur innerhalb des Streams senden, sodass eine Prüfung des Statuscodes ihn nie sieht. Die Lösung besteht darin, einen Stream nur dann als erfolgreich zu behandeln, wenn er mit seinem Finish-Signal endet, und bei einem sauberen Puffer neu zu starten, wenn er es nicht tut.

Der Rest dieser Seite zeigt, wie der kaputte Stream aussieht, zwei Python-Handler (Anthropic Messages und OpenAI SDK) und einen Retry, der den Anfang der Antwort nicht doppelt ausgibt. Beide Handler verwenden CheapTokens als API-Endpunkt: Die Dokumentation enthält die Basis-URLs, und das Beispielmodell ist DeepSeek-V4-Flash (ID deepseek-ai/DeepSeek-V4-Flash-0731, 380K Kontext, bis zu 32.768 Ausgabe-Tokens pro Antwort, ein Preis zu jeder Stunde — siehe die Modellseite).

Was eine 200 wirklich verspricht#

Stellen Sie sich einen Kurier vor, der an der Tür ein Paket entgegennimmt: Die Unterschrift beweist, dass er angekommen ist, nicht dass das Paket intakt ist. HTTP funktioniert genauso. Der Server sendet zuerst die Statuszeile und die Header, dann trifft der Body Stück für Stück ein. Wenn das Modell-Backend auf halbem Weg überlastet wird, ist die Statuszeile bereits verschickt.

Die Fehlerseite der Anthropic-API (geprüft am 1. November 2026) sagt es direkt: Wenn Sie eine Streaming-Antwort über SSE erhalten, kann ein Fehler auftreten, nachdem die API eine 200 zurückgegeben hat, und dann greift die Standard-Fehlerbehandlung nicht. Die Streaming-Dokumentation gibt die Form dieses Events an:

text
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

overloaded_error entspricht normalerweise HTTP 529 bei einem Nicht-Streaming-Aufruf (vorübergehende Überlastung durch hohen Traffic). Dieselbe Seite listet 500 api_error (Retry mit exponentiellem Backoff) und 504 timeout_error (Streaming für lange Anfragen verwenden). Auf der Leitung sieht ein kaputter Stream so aus:

Ein Stream, der mit 200 OK beginnt und mit event: error statt message_stop endet
Anatomie eines Mid-Stream-Fehlers: Die Content-Events hören auf, und event: error kommt dort an, wo message_stop stehen sollte (Form aus der Streaming-Dokumentation von Anthropic)

Die Dokumentation warnt außerdem, dass neue Event-Typen hinzugefügt werden können, sodass Ihr Code unbekannte Events problemlos verarbeiten sollte. Deshalb versuchen die Handler unten nicht, jedes Fehlerbyte zu erkennen. Sie stellen eine Frage: Endete der Stream mit seinem Finish-Signal?

  • Anthropic Messages: Das letzte Event ist message_stop, und stop_reason in message_delta sagt, warum er endete (end_turn, max_tokens, stop_sequence).
  • OpenAI Chat Completions: Der letzte Chunk trägt ein nicht-leeres finish_reason (stop, length, …).

Kein Finish-Signal oder eine Exception unterwegs — die Antwort ist unvollständig, egal was der Status gesagt hat.

In Python abfangen#

Nehmen Sie einen Support-Bot, der Antworten an den Browser streamt. Die Schleife sammelt Text in einem Puffer und meldet drei Dinge zurück: den bisherigen Text, ob der Stream beendet wurde und den Fehler, falls es einen gab. Wählen Sie den Tab für das SDK, das Sie verwenden.

Ein Handler, zwei SDKs

Anthropic SDK

python
import os
import anthropic

client = anthropic.Anthropic(
    base_url='https://api.cheaptokens.dev',  # no /v1 for the Anthropic SDK
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finished = [], False
    try:
        with client.messages.stream(
            model=MODEL,
            max_tokens=1024,
            messages=[{'role': 'user', 'content': prompt}],
        ) as stream:
            for event in stream:
                if event.type == 'content_block_delta' and event.delta.type == 'text_delta':
                    parts.append(event.delta.text)
                    on_text(event.delta.text)
                elif event.type == 'message_stop':
                    finished = True
    except anthropic.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finished, None

OpenAI SDK

python
import os
import openai

client = openai.OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finish = [], None
    try:
        stream = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            stream=True,
            messages=[{'role': 'user', 'content': prompt}],
        )
        for chunk in stream:
            if not chunk.choices:
                continue
            choice = chunk.choices[0]
            if choice.delta.content:
                parts.append(choice.delta.content)
                on_text(choice.delta.content)
            if choice.finish_reason:
                finish = choice.finish_reason
    except openai.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finish is not None, None

Zwei Details verdienen einen zweiten Blick. Erstens fangen beide Handler die Basis-Fehlerklasse des SDK ab (anthropic.APIError, openai.APIError), sodass es egal ist, welche Unterklasse das SDK für einen bestimmten Fehler wirft. Zweitens ist das Flag finished von der Exception getrennt: Ein Stream kann auch einfach früh enden, ganz ohne Exception, und dieser Fall muss ebenfalls als Fehler zählen.

Die README von openai-python ergänzt, was auf der OpenAI-Seite zu erwarten ist (geprüft am 1. November 2026): Ein Read-Timeout in einem Stream wirft APITimeoutError, andere HTTPX-Fehler werfen APIConnectionError mit der ursprünglichen Exception in __cause__. Wenn der Stream hängt statt fehlzuschlagen, siehe Timeouts und Retries im OpenAI Python SDK.

Retry ohne die Antwort doppelt auszugeben#

Hier ist ein Bug, der leicht ausgeliefert wird. Ein erster Versuch gibt „Die Rückerstattungsrichtlinie ist…“ aus, stirbt, der Retry beginnt von vorn und gibt „Die Rückerstattungsrichtlinie ist…“ erneut aus — jetzt sieht der Nutzer den Anfang zweimal oder einen Satz, der an seinen eigenen Neustart geklebt ist. Die übliche Ursache ist ein gemeinsamer Puffer: answer += text lebt außerhalb des Versuchs, sodass der zweite Versuch an den ersten anhängt.

Die Heilung ist langweilig. Ein Versuch besitzt einen Puffer, der Text eines fehlgeschlagenen Versuchs wird verworfen (oder nur für das Log behalten), und der Retry beginnt von vorn:

python
import time

def ask_with_retry(prompt, attempts=3, on_text=lambda t: None, on_restart=lambda: None):
    for attempt in range(attempts):
        text, finished, err = ask_once(prompt, on_text)
        if finished:
            return text
        status = getattr(err, 'status_code', None)
        if status is not None and 400 <= status < 500 and status not in (408, 409, 429):
            raise err  # a 400/401/404 will not fix itself
        print(f'attempt {attempt + 1} failed after {len(text)} chars: {err!r}')
        if attempt == attempts - 1:
            break
        on_restart()  # clear the message box in your UI
        time.sleep(2 ** attempt)  # 1 s, 2 s
    raise RuntimeError('stream failed after all attempts')

on_restart ist der Teil, der die Duplizierung stoppt. In einer Web-UI leert er die Nachrichtenblase, bevor der nächste Versuch hineinstreamt. In einem einfachen Terminal können Sie nicht un-printen, also geben Sie entweder erst aus, wenn finished wahr ist, oder drucken Sie eine sichtbare --- retrying ----Zeile. Ein Retry von vorn sendet den gesamten Prompt erneut, sodass die Eingabe für den neuen Versuch erneut berechnet wird; für die meisten Chat-Antworten ist das ein fairer Preis für eine saubere Antwort.

In die richtige Reihenfolge bringen

Bringen Sie die Retry-Schritte in die Reihenfolge, in der Ihr Code sie ausführen soll

  1. Den kaputten Stream nicht mehr lesen und seinen Teiltext für das Log behalten
  2. Den Fehler prüfen: Ein 4xx außer 408, 409 und 429 bedeutet Stopp, nicht Retry
  3. Die Nachrichtenbox leeren und einen neuen Puffer starten
  4. Mit exponentiellem Backoff warten (1 s, dann 2 s)
  5. Die vollständige Anfrage erneut senden und nur die neue Antwort anzeigen

Das Anthropic-SDK wiederholt bereits vorübergehende Fehler — Verbindungsfehler, Rate Limits, 5xx — mit exponentiellem Backoff, standardmäßig 2-mal, unter Beachtung von retry-after, und max_retries ändert das. Das deckt die Anfrage ab. Ein Stream, der nach der 200 stirbt, ist eine andere Geschichte: Die Dokumentation sagt, dass die Standard-Fehlerbehandlung dort nicht greift, also bleibt die obige Schleife Ihre Aufgabe. Für 429 allein gibt es eine separate Anleitung: 429 Too Many Requests: Retries mit Backoff.

Nicht jeder frühe Stopp ist ein Fehler#

Bevor Sie einen Retry machen, prüfen Sie, warum die Antwort endete. Ein Stream, der mit stop_reason: max_tokens (Anthropic) oder finish_reason: length (OpenAI) endet, ist beendet — er hat nur das von Ihnen gesetzte Limit erreicht. Ein Retry liefert denselben abgeschnittenen Text und dieselbe Rechnung. Erhöhen Sie stattdessen das Limit; der Leitfaden zur abgeschnittenen Antwort führt Sie durch.

Es gibt eine dritte Gruppe: das Netzwerk. Idle-Verbindungen können unterwegs verworfen werden, was sich als Verbindungsfehler oder als Stream zeigt, der ohne jegliches Event stoppt. Behandeln Sie das wie einen Mid-Stream-Fehler, und sehen Sie sich häufige Verbindungsfehler an, wenn es weiterhin passiert. Für lange Generierungen empfiehlt Anthropic Streaming gegenüber einem riesigen Nicht-Streaming-max_tokens, sodass Sie in dieser Hinsicht bereits auf der richtigen Seite sind.

Bevor Sie ausliefern#

Speichern Sie die Request-ID jedes fehlgeschlagenen Versuchs. Anthropic-Fehlerbodys tragen ein Feld request_id und einen Response-Header request-id, und das Python-SDK stellt message._request_id bereit; mit dieser ID ist ein Fehler nachvollziehbar statt anekdotisch.

Checkliste

Streaming-Fehlerbehandlung, richtig gemacht

Wenn Sie den Handler laufen sehen möchten, richten Sie ihn auf Ihren eigenen Key: einen API-Key in der Dokumentation erstellen, CHEAPTOKENS_API_KEY setzen und ask_with_retry('Explain SSE in two sentences') aufrufen. Für die Grundlagen des Streamings in Python beginnen Sie mit Streaming-Antworten in Python; die Anthropic-Seite wird in dem Anthropic Python SDK mit CheapTokens behandelt.

Retry-Ablauf: Stream startet, Fehler kommt an, Teiltext verworfen, Anfrage erneut gesendet
Die Retry-Schleife in fünf Schritten: starten, Fehler-Event, Teilpuffer verwerfen, Backoff, erneut fragen