{"record":{"id":"0b84582ff88fbdc1","repo":"star7th/showdoc","slug":"response-body-is-empty-server-did-not-return-a-st","errorCode":null,"errorMessage":"Response body is empty, server did not return a stream. Please try again later.","messagePattern":"Response body is empty, server did not return a stream\\. Please try again later\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web_src/src/api/aiAgent.ts","lineNumber":275,"sourceCode":"    let reader: ReadableStreamDefaultReader<any> | null = null\n    try {\n      const res = await fetch(url, {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n          Accept: 'text/event-stream',\n          'Cache-Control': 'no-cache',\n        },\n        body: JSON.stringify(body),\n        signal: controller.signal,\n      })\n\n      if (!res.ok) {\n        throw new Error(`HTTP ${res.status}: ${res.statusText}`)\n      }\n\n      if (!res.body) {\n        throw new Error('Response body is empty, server did not return a stream. Please try again later.')\n      }\n\n      reader = res.body.getReader()\n      const decoder = new TextDecoder('utf-8')\n      let buffer = ''\n\n      while (true) {\n        const { value, done } = await reader.read()\n        if (done) break\n\n        buffer += decoder.decode(value, { stream: true })\n\n        // SSE 按双换行分割\n        const parts = buffer.split('\\n\\n')\n        buffer = parts.pop() || ''\n\n        for (const part of parts) {\n          if (!part.trim()) continue","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/star7th/showdoc/blob/6a3fa91eee5ebf36ba9d9cba17e0fea6dfd4bb89/web_src/src/api/aiAgent.ts#L257-L293","documentation":"Thrown by sendAgentMessage() when the response to POST /api/agent/agent had an OK status but res.body (a ReadableStream) is null/undefined, so there is nothing to getReader() on and the SSE loop cannot run. fetch() returns a null body for body-less responses (204 No Content, 304) and in environments without streaming response support (very old browsers, some test setups/polyfills, or a service worker returning a synthetic Response built without a stream).","triggerScenarios":"Server (or an intermediate proxy) answering 200 with Content-Length: 0 or a 204 instead of the event-stream; PHP route echoing nothing before exit (e.g. fatal after SSE headers already sent as 200); a service worker or fetch polyfill intercepting the request and returning new Response() without a body; running the web_src app in an old webview where response.body is unsupported.","commonSituations":"A misconfigured proxy that swallows the streamed body but forwards the 200 status; a server code path that sends headers then dies before the first SSE chunk; error pages cached as empty 200s by an CDN/OPcache edge case; CI/component tests that mock fetch with a body-less Response.","solutions":["Reproduce with curl: `curl -N -X POST <host>/api/agent/agent -H 'Content-Type: application/json' -d '{...}'` and check whether any bytes stream back; if empty, debug the server route (PHP error log) rather than the client.","If a service worker is registered, bypass it for this request (`fetch(url, { ...opts })` inside a `navigator.serviceWorker.getRegistrations()` check) or make it pass the stream through untouched.","Confirm the deployment's proxy does not rewrite the response into an empty 200 (disable response buffering/compression for text/event-stream).","For old-webview support, feature-detect res.body and fall back to res.text() parsing instead of throwing immediately.","If it is transient (server restart mid-request), the built-in advice applies: retry after a short delay."],"exampleFix":"// before\nif (!res.body) {\n  throw new Error('Response body is empty, server did not return a stream. Please try again later.')\n}\nreader = res.body.getReader()\n\n// after\nif (!res.body) {\n  const text = await res.text()\n  if (text) {\n    // non-streaming fallback: parse the whole payload at once\n    for (const part of text.split('\\n\\n')) params.onEvent(JSON.parse(part.replace(/^data:\\s*/m, '')))\n    params.onDone()\n    return\n  }\n  throw new Error('Response body is empty, server did not return a stream. Please try again later.')\n}\nreader = res.body.getReader()","handlingStrategy":"fallback","validationCode":"// capability check before opening the agent stream (once at app startup)\nexport const supportsStreamingResponse =\n  typeof ReadableStream !== 'undefined' &&\n  'body' in new Response('')","typeGuard":"function hasStreamBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {\n  return res.body instanceof ReadableStream\n}","tryCatchPattern":"// existing structure already centralizes this: inside sendAgentMessage's IIFE catch,\n// treat the empty-body error as retriable (server hiccup) but cap attempts:\n} catch (e: any) {\n  if (e?.message?.includes('Response body is empty') && attempt < 2) {\n    attempt++\n    await new Promise(r => setTimeout(r, 1000 * attempt))\n    continue  // or re-invoke the IIFE\n  }\n  params.onError(e)\n}","preventionTips":["Feature-detect response.body support once and route old webviews to a non-streaming endpoint or res.text() parsing.","Verify the endpoint with curl -N during integration so empty 200s are caught before release.","Keep service workers from synthesizing body-less Responses for /api/agent/ requests.","Log Content-Type of the failed response; anything other than text/event-stream indicates a proxy or error page intercepting the stream."],"tags":["fetch","readable-stream","sse","empty-body","frontend"],"backgroundTag":"empty-response-body","analyzedSha":"6a3fa91eee5ebf36ba9d9cba17e0fea6dfd4bb89","analyzedAt":"2026-08-21T01:16:20.916Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}