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

OpenAI stream response has no body

Error message

OpenAI stream response has no body (status ${response.status})

What it means

AIError.ProviderResponseError thrown by postOpenAIStream when the OpenAI-compatible endpoint answered with an OK status but response.body is null/undefined, so SSE parsing cannot begin. This normally only happens with non-streaming-capable fetch implementations or interception layers that replace the body. It is thrown with kind "envelope" since the HTTP envelope itself is defective.

Solutions

  1. Inspect any custom fetch implementation passed to the client — ensure it returns a Response with a live readable body.
  2. Fix test mocks to construct `new Response(readableStream)` instead of a body-less Response.
  3. Remove caching/logging wrappers that consume response.body before returning it.
  4. If the server legitimately sends an empty 200 (shouldn't for streams), check the endpoint actually supports `stream: true`.

Example fix

// before: mock strips body
fetch: async () => new Response(null, { status: 200 });
// after: mock provides a readable SSE body
fetch: async () => new Response(sseStreamOf(chunk), { status: 200, headers: { "content-type": "text/event-stream" } });
Defensive patterns

Strategy: validation

Validate before calling

// when injecting a custom fetch (tests/proxies), assert body presence before use
const res = await fetch(url, init);
if (res.ok && !res.body) throw new Error("custom fetch returned 200 without a body");

Type guard

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

Try / catch

try {
	const stream = await client.stream(request);
} catch (err) {
	if (err instanceof AIError.ProviderResponseError && /no body/.test(err.message)) {
		// fix custom fetch shim / retry with default fetch
	} else throw err;
}

Prevention

When it happens

Trigger: A successful (response.ok) streaming POST where response.body is falsy — custom ctx.fetch shims that return a Response without a body, some polyfills/test mocks, or node-fetch-style environments where body was already consumed or not provided.

Common situations: Unit tests injecting mock Responses built without a body; custom fetch wrappers (logging/caching) that read and fail to re-attach the body; runtime polyfill mismatches (e.g. undici mock agents returning opaque responses).

Related errors


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

Appendix: source

Thrown at packages/ai/src/utils/openai-http.ts:111

		headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...init.headers },
		body: JSON.stringify(init.body),
		signal: init.signal,
		fetch: init.fetch,
		maxAttempts: DEFAULT_MAX_ATTEMPTS,
		// A proxy concurrency-admission 429 (`rate_limit_type: max_parallel_requests`)
		// surfaces immediately instead of being slept-and-retried here; session
		// recovery owns its backoff/fallback (issue #8854).
		shouldRetryResponse: (response, bodyText) => !isConcurrencyAdmissionRejection(response, bodyText),
		// Bun's native fetch enforces a hard ~300s pre-response timeout (issue #2422).
		// Cold large-context streams legitimately exceed it; the caller's
		// `firstEventTimeoutMs`/`AbortSignal` already govern stuck requests.
		timeout: false,
	});
	if (!response.ok) {
		throw await captureOpenAIHttpError(response);
	}
	if (!response.body) {
		throw new AIError.ProviderResponseError(`OpenAI stream response has no body (status ${response.status})`, {
			kind: "envelope",
		});
	}
	return {
		events: readSseJson<TEvent>(response.body, init.signal, init.onSseEvent),
		response,
		requestId: response.headers.get("x-request-id"),
	};
}

/** Decode a non-2xx response into an {@link OpenAIHttpError} without consuming it twice. */
export async function captureOpenAIHttpError(response: Response): Promise<AIError.OpenAIHttpError> {
	let bodyText: string | undefined;
	let bodyJson: unknown;
	try {
		bodyText = await response.text();
		if (bodyText.trim().length > 0) {
			try {

View on GitHub (pinned to 9690622007)