{"record":{"id":"5c538e8ab43d9b75","repo":"can1357/oh-my-pi","slug":"openai-stream-response-has-no-body-status-respo","errorCode":null,"errorMessage":"OpenAI stream response has no body (status ${response.status})","messagePattern":"OpenAI stream response has no body \\(status (.+?)\\)","errorType":"exception","errorClass":"AIError.ProviderResponseError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/utils/openai-http.ts","lineNumber":111,"sourceCode":"\t\theaders: { \"Content-Type\": \"application/json\", Accept: \"text/event-stream\", ...init.headers },\n\t\tbody: JSON.stringify(init.body),\n\t\tsignal: init.signal,\n\t\tfetch: init.fetch,\n\t\tmaxAttempts: DEFAULT_MAX_ATTEMPTS,\n\t\t// A proxy concurrency-admission 429 (`rate_limit_type: max_parallel_requests`)\n\t\t// surfaces immediately instead of being slept-and-retried here; session\n\t\t// recovery owns its backoff/fallback (issue #8854).\n\t\tshouldRetryResponse: (response, bodyText) => !isConcurrencyAdmissionRejection(response, bodyText),\n\t\t// Bun's native fetch enforces a hard ~300s pre-response timeout (issue #2422).\n\t\t// Cold large-context streams legitimately exceed it; the caller's\n\t\t// `firstEventTimeoutMs`/`AbortSignal` already govern stuck requests.\n\t\ttimeout: false,\n\t});\n\tif (!response.ok) {\n\t\tthrow await captureOpenAIHttpError(response);\n\t}\n\tif (!response.body) {\n\t\tthrow new AIError.ProviderResponseError(`OpenAI stream response has no body (status ${response.status})`, {\n\t\t\tkind: \"envelope\",\n\t\t});\n\t}\n\treturn {\n\t\tevents: readSseJson<TEvent>(response.body, init.signal, init.onSseEvent),\n\t\tresponse,\n\t\trequestId: response.headers.get(\"x-request-id\"),\n\t};\n}\n\n/** Decode a non-2xx response into an {@link OpenAIHttpError} without consuming it twice. */\nexport async function captureOpenAIHttpError(response: Response): Promise<AIError.OpenAIHttpError> {\n\tlet bodyText: string | undefined;\n\tlet bodyJson: unknown;\n\ttry {\n\t\tbodyText = await response.text();\n\t\tif (bodyText.trim().length > 0) {\n\t\t\ttry {","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/utils/openai-http.ts#L93-L129","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Inspect any custom fetch implementation passed to the client — ensure it returns a Response with a live readable body.","Fix test mocks to construct `new Response(readableStream)` instead of a body-less Response.","Remove caching/logging wrappers that consume response.body before returning it.","If the server legitimately sends an empty 200 (shouldn't for streams), check the endpoint actually supports `stream: true`."],"exampleFix":"// before: mock strips body\nfetch: async () => new Response(null, { status: 200 });\n// after: mock provides a readable SSE body\nfetch: async () => new Response(sseStreamOf(chunk), { status: 200, headers: { \"content-type\": \"text/event-stream\" } });","handlingStrategy":"validation","validationCode":"// when injecting a custom fetch (tests/proxies), assert body presence before use\nconst res = await fetch(url, init);\nif (res.ok && !res.body) throw new Error(\"custom fetch returned 200 without a body\");","typeGuard":"function hasBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {\n\treturn res.body !== null;\n}","tryCatchPattern":"try {\n\tconst stream = await client.stream(request);\n} catch (err) {\n\tif (err instanceof AIError.ProviderResponseError && /no body/.test(err.message)) {\n\t\t// fix custom fetch shim / retry with default fetch\n\t} else throw err;\n}","preventionTips":["Never return body-less Responses from custom fetch shims or test mocks.","Avoid wrappers that consume response.body before returning it.","Verify streaming endpoints are called with stream: true so a body is always produced."],"tags":["streaming","openai","http","response-parsing"],"backgroundTag":"empty-response-body","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}