← All posts

Guides

Why Do AI Streaming Errors Happen After HTTP 200?

HTTP 200 does not prove an AI stream finished. Check terminal events, preserve partial output, and separate retry attempts in your Token360 client.

STREAMING · RELIABILITY · BLOG 167

An AI stream can return HTTP 200 and still fail before the answer finishes. The status arrives before the full stream has been received. Your application needs to inspect subsequent events, the completion reason and the protocol’s terminal signal before treating partial text as a completed result.

This topic starts with OpenRouter’s FAQ question about how streaming works. The practical integration question is what to do when the response starts successfully but the user receives only part of the answer.

This article combines official documentation with an executed, eight-case local event-state fixture. The fixture uses synthetic events; it is not a Token360 outage test, a network benchmark or an estimate of model reliability. No paid inference requests were made.

Read first: Token360’s response protocol compatibility guide identifies the contract your parser should follow. The endpoint determines that contract, rather than the upstream provider selected behind it.

Why can a stream fail after HTTP 200?

The initial HTTP status and the final outcome describe different moments. Once response headers have been sent, a later failure cannot replace that status. The application may instead receive an error event or lose the connection. A successful start therefore needs a separate record from a successfully completed generation.

OpenRouter’s streaming documentation explicitly describes errors after the response is committed. Its error event format depends on the endpoint. That establishes a real integration hazard, but it does not establish that another gateway uses the same error fields. Implement against the selected service’s documented contract.

ObservationWhat it establishesWhat remains unknown
HTTP 200 receivedThe response began with a successful HTTP status.Whether generation and delivery will finish.
Text appearsSome content reached the application.Whether it is complete or usable.
Connection closesNo more data is arriving on that connection.Whether closure followed the expected terminal signal.
Normal terminal signal and acceptable completion reasonThe client has evidence of protocol completion.Whether the answer meets the task’s quality requirements.

For example, an assistant may begin “The three required documents are…” and disconnect after listing one. The text is meaningful and the HTTP status is successful, yet saving it as the final checklist would hide a missing deliverable.

What should a Token360 client inspect?

Track the stream using the endpoint’s documented events and fields, then translate them into your application’s own states. Keep received text separate from completion evidence. An adapter should record errors, finish information and the terminal signal, rather than treating every event containing text as progress toward an automatically successful response.

Token360 documents Chat Completions-compatible SSE chunks, Responses-compatible events and Messages-compatible events. Its protocol guide says Chat Completions clients should wait for [DONE], tolerate optional fields and avoid assumptions about chunk boundaries. A parser for one endpoint should not be reused unchanged for another.

The application design proposed here has three separate questions: did the transport end normally, did the model finish the expected kind of response, and did the result satisfy the user’s task? A tool handoff can be a legitimate next step without being the final user-facing answer. A length-limited response needs a different label from an ordinary completed answer.

Three checkpoints separate HTTP response start, stream completion evidence and acceptance of the answer
Recommended application checkpoints. These labels are internal design choices, not Token360 API field names.

Do not let a terminal marker erase an earlier error. Preserve all relevant observations until classification. Equally, do not conclude that a well-formed, completed answer is factually correct: business validation remains a separate step.

How can you test completion handling without spending tokens?

Feed deliberately constructed event sequences into the part of your client that decides whether an answer is finished. Include successful endings, errors, interruptions and non-answer handoffs. This isolates a client-side decision rule without asking a live model to fail on demand or confusing model behavior with application logic.

We ran the following eight cases locally on November 6, 2026. Every sequence represents an already-open response. Events are normalized labels such as text, finish and terminal, not captured provider payloads. The fixture deliberately does not implement HTTP, SSE framing, UTF-8 decoding, concurrent requests or tool execution. It assumes one response stream with one output choice and normalized events in valid order. It does not validate event ordering or separate interleaved choices; those require adapter-level tests.

Synthetic sequenceActual local classificationRecommended UI treatment
Text → stop → terminalcomplete_candidateAllow task-level validation.
Text → connection endsincompleteLabel the fragment incomplete.
Error before textfailedShow failure, not a blank success.
Text → error → terminalfailedKeep the error despite the terminal signal.
Text → length limit → terminallimitedExplain that output hit a limit.
Tool handoff → terminaltool_handoffContinue the approved tool workflow.
Heartbeat → connection endsincompleteDo not count a heartbeat as an answer.
Text → terminal, no finish informationneeds_reviewApply the endpoint-specific policy.

All eight returned their expected classifications. That verifies these eight assertions in this illustrative classifier; it is not an eight-out-of-eight model success rate or proof of production readiness. The last case is deliberately conservative: your actual adapter must account for the completion evidence the chosen endpoint provides.

Copy the

Copy stream-state-fixture.py:

"""Synthetic normalized events; NOT a wire parser or captured API traffic."""
import json
from pathlib import Path

def classify(events):
    text = ''; terminal = False; reason = None; error = False
    for kind, value in events:
        if kind == 'text': text += value
        elif kind == 'finish': reason = value
        elif kind == 'error': error = True
        elif kind == 'terminal': terminal = True
    if error: state = 'failed'
    elif not terminal: state = 'incomplete'
    elif reason == 'tool_calls': state = 'tool_handoff'
    elif reason == 'length': state = 'limited'
    elif reason == 'stop' and text: state = 'complete_candidate'
    else: state = 'needs_review'
    return state

cases = [
 ('normal text', [('text','Hello'),('finish','stop'),('terminal',None)], 'complete_candidate'),
 ('text then disconnect', [('text','Partial')], 'incomplete'),
 ('error before text', [('error','upstream')], 'failed'),
 ('text then error then terminal', [('text','Partial'),('error','upstream'),('terminal',None)], 'failed'),
 ('limit then terminal', [('text','Partial'),('finish','length'),('terminal',None)], 'limited'),
 ('tool handoff', [('finish','tool_calls'),('terminal',None)], 'tool_handoff'),
 ('heartbeat then disconnect', [('heartbeat',None)], 'incomplete'),
 ('terminal without finish', [('text','Maybe'),('terminal',None)], 'needs_review'),
]
results=[]
for name, events, expected in cases:
    actual=classify(events)
    assert actual==expected, (name,actual)
    results.append({'case':name,'expected':expected,'actual':actual})
Path(__file__).with_name('fixture-results.json').write_text(json.dumps(results,indent=2))
print(json.dumps(results,indent=2))

and its

Copy fixture-results.json:

[
  {
    "case": "normal text",
    "expected": "complete_candidate",
    "actual": "complete_candidate"
  },
  {
    "case": "text then disconnect",
    "expected": "incomplete",
    "actual": "incomplete"
  },
  {
    "case": "error before text",
    "expected": "failed",
    "actual": "failed"
  },
  {
    "case": "text then error then terminal",
    "expected": "failed",
    "actual": "failed"
  },
  {
    "case": "limit then terminal",
    "expected": "limited",
    "actual": "limited"
  },
  {
    "case": "tool handoff",
    "expected": "tool_handoff",
    "actual": "tool_handoff"
  },
  {
    "case": "heartbeat then disconnect",
    "expected": "incomplete",
    "actual": "incomplete"
  },
  {
    "case": "terminal without finish",
    "expected": "needs_review",
    "actual": "needs_review"
  }
]

. Run it with python3 stream-state-fixture.py; it uses only the Python standard library and makes no network calls. Extend it with sanitized real events before using the design in production.

How should the interface handle partial output?

Keep partial output visible only with a clear unfinished state, and prevent downstream actions that assume a completed answer. A user can still inspect or copy a fragment, but the application should not silently approve, export or execute it as final. Define those restrictions according to the consequence of the task.

In a drafting tool, a useful message is “Generation interrupted; this draft may be incomplete.” In a procurement workflow, leave the recommendation unapproved. In a structured extraction workflow, avoid inserting a truncated record into the operational database simply because its opening fields were valid.

Use separate records for the conversation turn and each generation attempt. For instance, turn T-17 can contain attempts A-1 and A-2, each with its own output, completion state and correlation IDs. That makes a retry reviewable without replacing the evidence of the original interruption. These identifiers are illustrative application IDs, not prescribed API fields.

Should you retry and append the new text?

Do not append a fresh generation to a failed fragment by default. A new attempt can produce different wording, assumptions or structure, so joining the outputs may create an answer that neither attempt actually produced. Choose explicitly between replacing the draft, asking for a continuation or escalating the failure for review.

Token360’s interrupted-stream guidance says to treat closure before the normal terminal event as incomplete, label or discard partial output, and consider duplicate work and cost before retrying. It also recommends bounded retry behavior and retaining correlation information. That is a recovery policy, not a guarantee that retrying fixes the cause.

For a simple text draft, replacement may be acceptable. For an agent that can change external state, first determine whether any action already happened. A second model request does not roll back a tool action, and an interrupted answer is not evidence that all earlier steps failed.

If you offer “Continue,” disclose that it starts another generation using the context you supply. Do not describe it as byte-exact resumption of the original stream unless the endpoint explicitly provides and verifies that capability.

What should you record to investigate an interruption?

Record enough lifecycle evidence to distinguish a response that never delivered text from one that stopped midway. Keep the HTTP status, endpoint, public model, attempt ID, available request correlation IDs, observed finish information and whether a terminal signal arrived. Add timestamps without treating any one timestamp as proof of completion.

A compact record can include headers_received_at, first_text_at, last_event_at, terminal_seen, error_seen and application_state. These are proposed internal fields. Use a consistent clock for elapsed durations and retain a UTC wall-clock time for support correlation.

Keep usage accounting separate from stream classification. If an interrupted connection prevents the client from receiving usage data, record usage as unknown pending reconciliation. Do not replace it with zero. Avoid including authorization headers or raw sensitive prompts in a diagnostic export.

For an enterprise deployment through Token360, the practical acceptance check is whether the application can distinguish started, interrupted and accepted work. The platform’s enterprise focus does not remove the need to verify those client behaviors for the endpoint and model used.

For the adjacent diagnostic, inspect an apparently empty or interrupted response. Keep that question separate from the checks above.

Frequently asked questions

Does a complete-looking sentence prove that streaming finished?

No. A fragment can end at a sentence boundary by chance. Use the completion evidence supplied by the protocol, then assess whether the requested task was fulfilled.

Does every interrupted stream require another request?

No. A user may cancel, retain a clearly labeled fragment or ask for human review. Retrying is a product decision constrained by cause, budget and possible duplicate actions.

Can these local tests tell me which model disconnects least?

No. They test a small application classifier with synthetic events. Comparing real reliability requires actual requests, a defined workload and a consistent measurement window.

Read next: use Token360’s timeout and retry guidance to set a bounded recovery policy. Revisit the endpoint contract when changing APIs so that the same interface does not conceal different completion semantics.

  • Developer Guide
  • API

Build faster with one AI API.

Use Token360 to call video, image, audio, and text models with one key and one bill.

Get started