Skip to content

stream disconnected before completion: failed to parse ErrorResponse: invalid type: null, expected struct Error

Two separate things went wrong here, and the second one is why you cannot see the first. A streamed response ended early; the client then went to read the error body so it could tell you why, and the body did not match the shape it expected. What you are reading is a deserializer’s complaint, not a message from the platform. That is why you cannot look this up as an API error — there is no type and no code in it, because extracting exactly those fields is the step that failed. Whatever the real cause was, it is still unknown to you, and recovering it is what this page is about.

Read the type error literally

invalid type: null, expected struct Error is precise, and the precision is usable.

It means the bytes did parse as JSON. It means the top-level shape matched closely enough that the client reached a field it believed would hold an error object. And it means that field contained null.

Rule out what that excludes. A proxy returning an HTML error page would fail far earlier, with a complaint about the first character rather than about a field’s type. A truncated body would fail with an unexpected end of input. You received a complete, well-formed, JSON document whose error slot was empty. Something produced an envelope shaped like an API response and had nothing to put in it.

Whose schema is the client actually enforcing?

Here is the judgment that reframes the whole problem, and it comes from what the vendor documentation does not say.

OpenAI’s error-codes documentation names the fields of an error individually — error.type, error.code, error.param, error.message — and describes them in prose, but it does not publish the literal response body those fields live in. So the ErrorResponse struct your client is trying to fill was inferred from observed responses, not generated from a published schema. It encodes one party’s understanding of a shape nobody has formally specified.

That makes the struct brittle in one specific direction: against anything that emits a compatible but not identical envelope. And there are more of those in a typical setup than people expect — an API-compatible deployment on another cloud, a corporate gateway that normalises responses, a routing proxy, a relay that adds retries, an aggregator that fronts several providers. The vendor’s own documentation set is explicit that it does not cover Azure OpenAI or Amazon Bedrock, and names different error bodies as one of the reasons. A client built against the first-party shape meeting a near-miss shape is exactly how you get null where a struct was expected.

So the first question this error should raise is not “why did the stream die” but “am I certain about who answered me?” If a base URL, a proxy setting, an enterprise gateway or a compatibility layer sits between your client and the platform, that is your leading candidate, and it explains both halves of the message at once: the intermediary ended the stream, then produced its own error envelope to explain it, and the envelope did not match.

Is retrying useful?

Yes — once, and treat it as a measurement rather than a fix.

The underlying failure is invisible, so you cannot reason about whether it was transient. A single retry resolves that for you at low cost, and the two outcomes mean different things.

The retry succeeds. The original failure was transient and the empty envelope was a one-off — typically an intermediary under momentary load producing a placeholder error. Note it and move on.

The retry fails identically. You have proven the behaviour is deterministic. A deterministic unparseable error is not a network condition; it means whichever component answers you always produces that shape for this class of failure. Further identical attempts will reproduce it exactly, and failed requests still count against your per-minute limits, so the loop is not free.

There is no automatic retry underneath you to lean on here, either. The official Python client’s documentation states that stream consumption is not automatically retried, because replaying a request could duplicate output already delivered to the application. Any retry around a stream is one you or your tool wrote — and the same warning applies to it: if your code already acted on partial output, a replay makes those side effects happen twice.

Get the bytes before you get anything else

Everything below depends on one action, and it is not a configuration change. Capture the raw response body and the HTTP status underneath the client that is failing to parse them. Log at the transport layer, or put a local debugging proxy in the path, and keep the bytes verbatim.

That single capture usually ends the investigation, because the document you recover names its author. A first-party error carries the documented fields. An intermediary’s error carries its own vocabulary — a gateway name, an upstream identifier, a vendor-specific code. A body with error: null and a status attached tells you which layer generated it and what it thought was happening.

While you are there, read the x-request-id response header. For failed requests the official SDKs expose it on the caught error — .request_id in Python, .requestID in Node — and values look like req_123. If the failing response has no such header at all, that is itself evidence that the response did not come from where you think it did.

Telling the causes apart

Is anything between you and the platform? Check the configured base URL, proxy environment variables, and any enterprise routing your organisation applies. This is the cheapest test and the highest-yield one.

Do requests you know are invalid also come back unreadable? This is the discriminating test worth running. Deliberately send a request the platform would reject outright — a nonexistent model name, for instance — and see what your client prints. If you get a clean, typed error with a code, the error path works and the streaming failure produced something unusual. If the deliberate rejection also fails to parse, the responder’s error envelope is systematically incompatible with your client, and the stream disconnection was incidental. Those two findings send you to completely different fixes.

Does it only happen under load or on long turns? An intermediary producing placeholder errors when it runs out of capacity fits that pattern. The empty error slot is what a component writes when it has to fail a request it never got an answer for.

Did it start after a change? A new gateway, a routing change, a client upgrade, a different deployment target. A struct mismatch appearing on a day nothing changed on your side means something changed on the other side.

Fix by scenario

  • An intermediary is in the path — route around it for one test. If the same workload against the platform directly produces readable errors, you have found the component and the conversation moves to whoever operates it.
  • Deliberate rejections also fail to parse — your client and your endpoint disagree about the error format. Stop debugging the stream; this is a compatibility problem and it affects every failure you will ever see, not just this one.
  • Transient, clears on retry — leave it. Add the body capture anyway so the next occurrence is readable without a second investigation.
  • Deterministic, no intermediary you know of — the capture is mandatory, not optional. Report it with the raw body, the status code and the request id; without those, a bug report against the client is a description of a feeling.
  • You need to keep working meanwhile — if the same request succeeds without streaming, use that path temporarily. A non-streamed failure comes back as an ordinary HTTP error, which your client parses through a different code path and often reports correctly.

How to confirm it’s fixed

Do not wait for the next organic failure — force one. Send the deliberately invalid request again and require your client to print a real error with a code in it. That is falsifiable in a way “it hasn’t happened lately” is not: either the error path now yields a readable document or it does not, and you find out in one request instead of one week.

Then re-run the workload that was failing, at the same size, on the same path, and require it to complete twice in a row. One success is indistinguishable from a transient condition clearing on its own while you were editing configuration.

The neighbours in this cluster differ by how much of the failure survived. When the stream ends with no completion event and no error body at all — nothing to parse rather than something unparseable — you are looking at stream closed before response.complete, where absence is the evidence and there is nothing to recover. When the client caught a real exception and flattened it into a sentence, An error occurred during streaming covers recovering the typed event that a blanket handler threw away. When the transport reports that the body ended while bytes were still owed, the framing broke rather than the schema; that is Response payload is not completed, which keys off whether the byte count or the elapsed time repeats. If you are not sure which of them you are holding, the AI coding error triage tool sorts them by the signals used above: whether a body arrived, whether it parsed, and whether any code survived.