{"record":{"id":"b964e5dd77dc3a4c","repo":"unslothai/unsloth","slug":"stream-response-missing-body","errorCode":null,"errorMessage":"Stream response missing body","messagePattern":"Stream response missing body","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"studio/frontend/src/features/chat/api/chat-api.ts","lineNumber":1317,"sourceCode":"\nexport async function* streamChatCompletions(\n  payload: OpenAIChatCompletionsRequest,\n  signal: AbortSignal,\n): AsyncGenerator<OpenAIChatChunk> {\n  const response = await authFetch(\"/v1/chat/completions\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(payload),\n    signal,\n  });\n\n  if (!response.ok) {\n    const body = await response.json().catch(() => null);\n    throw new Error(parseErrorText(response.status, body));\n  }\n\n  if (!response.body) {\n    throw new Error(\"Stream response missing body\");\n  }\n\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n  let completed = false;\n  // EOF without `[DONE]` or a finish_reason chunk means the stream was cut mid-generation.\n  let sawTerminalSignal = false;\n  let terminalFinishReason: string | null = null;\n  let sawAssistantContent = false;\n  let sawReasoningContent = false;\n\n  const throwIfReasoningOnlyLength = () => {\n    if (\n      terminalFinishReason === \"length\" &&\n      sawReasoningContent &&\n      !sawAssistantContent\n    ) {","sourceCodeStart":1299,"sourceCodeEnd":1335,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/frontend/src/features/chat/api/chat-api.ts#L1299-L1335","documentation":"Thrown when the /v1/chat/completions response is ok (2xx) but response.body is null, so there is no ReadableStream to read SSE events from. This guards the invariant that a streaming completions call must return a body. It fires before any chunk is parsed, so no partial transcript exists.","triggerScenarios":"A 2xx response with no body: an intermediary (proxy, service worker, mock fetch) stripping the stream; a server bug returning Content-Length: 0 with 200; a fetch polyfill or test double that omits body.","commonSituations":"Corporate proxies or middleware that buffer/empty streaming responses; service workers intercepting fetch; unit tests mocking fetch with new Response() lacking a body; HTTP/1.0 intermediaries that drop chunked encoding.","solutions":["Check whether a proxy, service worker, or fetch shim sits between the app and the inference endpoint and strips response bodies.","Verify the server actually sets Content-Type: text/event-stream and streams a body for this request.","In tests, mock the Response with a real ReadableStream body."],"exampleFix":"// before (broken test mock)\nconst res = new Response('', { status: 200 });\n\n// after\nconst res = new Response(sseEncoder.encode('data: [DONE]\\n\\n'), { status: 200 });","handlingStrategy":"validation","validationCode":"if (!response.body) {\n  // detect stripped streams early: check content-type before reading\n  const ct = response.headers.get('content-type') ?? '';\n  if (!ct.includes('event-stream')) console.warn('non-SSE content-type:', ct);\n}","typeGuard":null,"tryCatchPattern":"try { yield* readStream(); } catch (e) { if (e.message === 'Stream response missing body') hintAtProxyIssue(); throw e; }","preventionTips":["Ensure streaming endpoints bypass buffering proxies (gzip off, chunked allowed).","In tests, always construct Response with a real ReadableStream.","Register service workers with a pass-through for /v1/chat/completions."],"tags":["streaming","sse","fetch","invariant"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}