Bumalik sa blog

Pag-troubleshootGabay

Nabigo ang stream pagkatapos ng HTTP 200: hulihin ang mid-stream error sa Python

Bakit maaaring masira ang stream pagkatapos ng 200 status, paano makita ang event: error, at isang Python retry na nagpapanatili ng partial text nang hindi nadodoble ang sagot.

Ni CheapTokens TeamNai-publish noong 7 min na pagbabasa

Stream na may HTTP 200 OK na nagtatapos sa event: error at overloaded_error sa halip na message_stop
Sa pahinang ito
  1. Ano talaga ang ipinapangako ng 200
  2. Hulihin ito sa Python
  3. Mag-retry nang hindi napi-print ang sagot nang dalawang beses
  4. Hindi lahat ng maagang paghinto ay error
  5. Bago ka mag-ship

Ang iyong chat UI ay nag-stream ng kalahating sagot, nag-freeze, at sinasabi ng log na 200 OK. Hindi iyon kontradiksyon: ang 200 ay nagsasabi lamang na nagsimula ang stream, hindi na natapos ito. Kung may masira mamaya, ang server ay makakapagpadala lamang ng error sa loob ng stream, kaya ang pagsuri sa status code ay hindi ito nakikita. Ang solusyon ay ituring na matagumpay ang isang stream lamang kapag nagtatapos ito sa finish signal nito, at mag-retry mula sa malinis na buffer kapag hindi.

Ang natitirang bahagi ng pahinang ito ay nagpapakita kung ano ang hitsura ng sirang stream, dalawang Python handler (Anthropic Messages at OpenAI SDK), at isang retry na hindi nagpi-print ng simula ng sagot nang dalawang beses. Pareho ginagamit ng mga handler ang CheapTokens bilang API endpoint: ang docs ay may base URLs, at ang sample na modelo ay DeepSeek-V4-Flash (id deepseek-ai/DeepSeek-V4-Flash-0731, 380K context, hanggang 32,768 output tokens bawat sagot, isang presyo sa anumang oras — tingnan ang pahina ng modelo).

Ano talaga ang ipinapangako ng 200#

Isipin ang isang courier na pumipirma ng parcel sa pinto: ang pirma ay nagpapatunay na nakarating siya, hindi na buo ang parcel. Ganito rin ang HTTP. Ang server ay nagpapadala muna ng status line at headers, pagkatapos ang body ay dumarating nang paisa-isa. Kung ang model backend ay ma-overload sa kalagitnaan, wala na ang status line.

Ang API errors page ng Anthropic (sinuri noong 1 Nobyembre 2026) ay direktang nagsasabi: kapag nakatanggap ka ng streaming response sa SSE, maaaring mangyari ang error pagkatapos magbalik ng 200 ang API, at hindi na naaangkop ang standard error handling. Ibinibigay ng streaming docs nito ang hugis ng event na iyon:

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

Ang overloaded_error ay karaniwang tumutugma sa HTTP 529 sa non-streaming call (pansamantalang overload mula sa mataas na traffic). Ang parehong pahina ay naglilista ng 500 api_error (retry na may exponential backoff) at 504 timeout_error (gumamit ng streaming para sa mahahabang request). Sa wire, ganito ang hitsura ng sirang stream:

Isang stream na nagsisimula sa 200 OK at nagtatapos sa event: error sa halip na message_stop
Anatomy ng mid-stream error: humihinto ang content events at dumarating ang event: error kung saan dapat ang message_stop (hugis mula sa streaming docs ng Anthropic)

Nagbabala rin ang docs na maaaring magdagdag ng mga bagong event type, kaya dapat hawakan ng iyong code ang mga hindi kilalang event nang maayos. Iyon ang dahilan kung bakit ang mga handler sa ibaba ay hindi sinusubukang kilalanin ang bawat error byte. Isang tanong lang ang tinatanong nila: natapos ba ang stream sa finish signal nito?

  • Anthropic Messages: ang huling event ay message_stop, at ang stop_reason sa message_delta ay nagsasabi kung bakit natapos (end_turn, max_tokens, stop_sequence).
  • OpenAI Chat Completions: ang huling chunk ay may dalang non-empty finish_reason (stop, length, …).

Walang finish signal, o may exception sa daan — hindi kumpleto ang sagot, anuman ang sinabi ng status.

Hulihin ito sa Python#

Kumuha ng support bot na nag-stream ng mga reply sa browser. Kinokolekta ng loop ang text sa isang buffer at nag-uulat ng tatlong bagay: ang text sa ngayon, kung natapos ang stream, at ang error kung mayroon. Piliin ang tab para sa SDK na ginagamit mo.

Isang handler, dalawang SDK

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

Dalawang detalye ang dapat tingnan muli. Una, hinuhuli ng parehong handler ang base error class ng SDK (anthropic.APIError, openai.APIError), kaya hindi mahalaga kung aling subclass ang itataas ng SDK para sa isang failure. Pangalawa, ang finished flag ay hiwalay sa exception: ang isang stream ay maaari ring matapos nang maaga, na walang exception, at ang kaso na iyon ay dapat ituring ding failure.

Idinadagdag ng openai-python README ang aasahan sa panig ng OpenAI (sinuri noong 1 Nobyembre 2026): ang read timeout sa isang stream ay nagtataas ng APITimeoutError, ang iba pang HTTPX failure ay nagtataas ng APIConnectionError kasama ang orihinal na exception sa __cause__. Kung ang stream ay tumitigil sa halip na mag-fail, tingnan ang timeouts at retries sa OpenAI Python SDK.

Mag-retry nang hindi napi-print ang sagot nang dalawang beses#

Narito ang isang bug na madaling ma-ship. Ang unang pagtatangka ay nagpi-print ng "The refund policy is…", namatay, ang retry ay nagsimulang muli at nagpi-print ng "The refund policy is…" muli — ngayon nakikita ng user ang simula nang dalawang beses, o isang pangungusap na nakadikit sa sarili nitong restart. Ang karaniwang dahilan ay isang shared buffer: ang answer += text ay nasa labas ng pagtatangka, kaya ang pangalawang subok ay nagdadagdag sa una.

Ang lunas ay boring. Isang pagtatangka ang may-ari ng isang buffer, ang text ng nabigong pagtatangka ay itinatapon (o itinatago lamang para sa log), at ang retry ay nagsisimula mula sa simula:

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')

Ang on_restart ang bahaging pumipigil sa duplicate. Sa isang web UI, binabakante nito ang message bubble bago mag-stream ang susunod na pagtatangka. Sa isang plain terminal, hindi mo maaaring i-unprint, kaya maaaring mag-print lamang pagkatapos maging totoo ang finished, o mag-print ng nakikitang --- retrying --- na linya. Ang pag-retry mula sa simula ay muling nagpapadala ng buong prompt, kaya ang input ay sisingilin muli para sa bagong pagtatangka; para sa karamihan ng chat reply, patas na presyo iyon para sa malinis na sagot.

Ilagay sa tamang pagkakasunod-sunod

Ilagay ang mga hakbang sa retry sa pagkakasunod-sunod na dapat patakbuhin ng iyong code

  1. Itigil ang pagbasa sa sirang stream at itago ang partial text nito para sa log
  2. Suriin ang error: ang 4xx maliban sa 408, 409 at 429 ay nangangahulugang itigil, hindi mag-retry
  3. I-clear ang message box at magsimula ng bagong buffer
  4. Maghintay gamit ang exponential backoff (1 s, pagkatapos 2 s)
  5. Ipadala muli ang buong request at ipakita lamang ang bagong sagot

Ang SDK ng Anthropic ay nagre-retry na ng mga transient failure — connection error, rate limit, 5xx — gamit ang exponential backoff, 2 beses bilang default, na gumagalang sa retry-after, at binabago iyon ng max_retries. Saklaw nito ang request. Ang stream na namamatay pagkatapos ng 200 ay ibang usapan: sinasabi ng docs na hindi naaangkop doon ang standard error handling, kaya ang loop sa itaas ay sa iyo. Para sa 429 lang, may hiwalay na walkthrough: 429 Too Many Requests: retries with backoff.

Hindi lahat ng maagang paghinto ay error#

Bago ka mag-retry, suriin kung bakit natapos ang sagot. Ang stream na nagtatapos sa stop_reason: max_tokens (Anthropic) o finish_reason: length (OpenAI) ay natapos — naabot lang nito ang limit na itinakda mo. Ang pag-retry niyan ay magbabalik ng parehong putol na text at parehong bill. Itaas ang limit sa halip; ang gabay sa putol na sagot ay naglalakad dito.

May ikatlong grupo: ang network. Ang mga idle connection ay maaaring ma-drop sa daan, na lumalabas bilang connection error o stream na humihinto nang walang anumang event. Pangasiwaan ito tulad ng mid-stream error, at tingnan ang mga karaniwang connection error kung paulit-ulit itong nangyayari. Para sa mahahabang generation, inirerekomenda ng Anthropic ang streaming kaysa sa napakalaking non-streaming max_tokens, kaya nasa tamang panig ka na niyan.

Bago ka mag-ship#

I-save ang request id ng bawat nabigong pagtatangka. Ang error body ng Anthropic ay may dalang request_id field at request-id response header, at inilalantad ng Python SDK ang message._request_id; sa id na iyon, ang isang failure ay matutunton sa halip na anecdotal.

Listahan ng dapat gawin

Streaming error handling, ginawa nang tama

Kung gusto mong makita ang handler na tumatakbo, ituro ito sa sarili mong key: gumawa ng API key sa docs, itakda ang CHEAPTOKENS_API_KEY, at tawagan ang ask_with_retry('Explain SSE in two sentences'). Para sa mga pangunahing kaalaman ng streaming sa Python, magsimula sa streaming responses sa Python; ang panig ng Anthropic ay saklaw sa ang Anthropic Python SDK kasama ang CheapTokens.

Retry flow: nagsimula ang stream, dumarating ang error, itinatapon ang partial text, ipinapadala muli ang request
Ang retry loop sa limang hakbang: simula, error event, itapon ang partial buffer, mag-back off, magtanong muli