can1357/oh-my-pi · error · AIError.ProviderResponseError

Google API returned an empty response body

Error message

Google API returned an empty response body

What it means

streamGoogleGenAI received a response with ok status but a null/undefined body, so there is no SSE stream to consume. The library throws AIError.ProviderResponseError with kind "empty-body" because a successful streaming response always has a body.

Source

Thrown at packages/ai/src/providers/google-shared.ts:990

			const bodyJson = JSON.stringify(paramsToWireBody(params));
			const fetchImpl = plan.fetch ?? options?.fetch ?? (globalThis.fetch.bind(globalThis) as FetchImpl);
			const openStreamAt = async (requestUrl: string): Promise<ReadableStream<Uint8Array>> => {
				const response = await fetchImpl(requestUrl, {
					method: "POST",
					headers: { ...plan.headers, "Content-Type": "application/json", Accept: "text/event-stream" },
					body: bodyJson,
					signal: options?.signal,
				});
				if (!response.ok) {
					const errorText = await response.text().catch(() => "");
					throw new AIError.GoogleApiError(
						`Google API error (${response.status}): ${extractGoogleErrorMessage(errorText)}`,
						response.status,
						{ headers: response.headers },
					);
				}
				if (!response.body) {
					throw new AIError.ProviderResponseError("Google API returned an empty response body", {
						provider: model.provider,
						kind: "empty-body",
					});
				}
				return response.body as ReadableStream<Uint8Array>;
			};
			// A regional Vertex endpoint 404s for models published only on the
			// global endpoint; retry global once so a stale/ambient region never
			// breaks a request that worked before regional routing existed.
			const openStream = async (): Promise<ReadableStream<Uint8Array>> => {
				if (!plan.fallbackUrl) return openStreamAt(plan.url);
				try {
					return await openStreamAt(plan.url);
				} catch (error) {
					if (error instanceof AIError.GoogleApiError && error.status === 404) {
						return openStreamAt(plan.fallbackUrl);
					}
					throw error;

View on GitHub (pinned to 9690622007)

Solutions

  1. If in tests, fix the fetch mock to return a ReadableStream body
  2. Retry the request — rare in production and usually transient
  3. Inspect proxies/middlewares between the client and Google that could drop the body
  4. Catch AIError.ProviderResponseError with kind "empty-body" and fall back to a non-streaming request

Example fix

// before
// test mock
fetch.mockResolvedValue({ ok: true, status: 200 });
// after
fetch.mockResolvedValue({
  ok: true,
  status: 200,
  body: new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode('data: {...}\n\n')); c.close(); } }),
  headers: new Headers({ "content-type": "text/event-stream" }),
});
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

function hasBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
  return res.body !== null && res.body !== undefined;
}

Try / catch

try {
  return await streamGoogle(model, params, { signal });
} catch (err) {
  if (err instanceof AIError.ProviderResponseError && err.context?.kind === "empty-body") {
    return withBackoff(() => streamGoogle(model, params, { signal }), 2);
  }
  throw err;
}

Prevention

When it happens

Trigger: HTTP 200 with empty body — typically caused by an intermediary (proxy, service worker, mocked fetch in tests) stripping the body, or an edge node returning an empty success.

Common situations: Tests with incomplete fetch mocks (status set but body omitted); corporate proxies returning empty 200s; intermittent CDN/load-balancer bugs.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/f64edccd37ce85ca. Report an issue: GitHub.