{"record":{"id":"6f4a8444a292bfa9","repo":"datawhalechina/hello-agents","slug":"error-6f4a84","errorCode":null,"errorMessage":"无法读取响应流","messagePattern":"无法读取响应流","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"Co-creation-projects/lcyting-StockSage-agent/frontend/src/views/StockAnalysis.vue","lineNumber":1053,"sourceCode":"      },\n      body: JSON.stringify({\n        stock_code: stockCode.value.trim(),\n        stock_name: (stockName.value || '').trim(),\n      }),\n    })\n    if (!res.ok) {\n      const t = await res.text()\n      let msg = t || res.statusText\n      try {\n        const j = JSON.parse(t)\n        if (j.message) msg = j.message\n      } catch {\n        /* ignore */\n      }\n      throw new Error(msg)\n    }\n    const reader = res.body?.getReader()\n    if (!reader) throw new Error('无法读取响应流')\n\n    const dec = new TextDecoder()\n    let buf = ''\n    let streamError = null\n    let streamFinished = false\n\n    while (true) {\n      const { done: readerDone, value } = await reader.read()\n      if (value) buf += dec.decode(value, { stream: true })\n\n      let nl\n      while ((nl = buf.indexOf('\\n')) >= 0) {\n        const line = buf.slice(0, nl).trim()\n        buf = buf.slice(nl + 1)\n        if (!line) continue\n        let obj\n        try {\n          obj = JSON.parse(line)","sourceCodeStart":1035,"sourceCodeEnd":1071,"githubUrl":"https://github.com/datawhalechina/hello-agents/blob/606a07d341a47be773fab7f4b71177f53f96b2c3/Co-creation-projects/lcyting-StockSage-agent/frontend/src/views/StockAnalysis.vue#L1035-L1071","documentation":"Thrown in StockAnalysis.vue when res.ok is true but res.body is null/undefined, so getReader() cannot be created — '无法读取响应流' (cannot read response stream). It guards the NDJSON-line streaming loop that follows; without a reader there is no way to consume the incremental analysis deltas. Unlike the !res.ok branch, this fires on a 200 response whose body was stripped or never provided.","triggerScenarios":"The stream endpoint returns 200 with no body: proxy or middleware that buffers and closes the stream, a backend handler that returns Response without a body on some path, an environment where fetch response streaming is unavailable (resp.body undefined in old browsers/WebViews), or an opaque response mode (mode:'opaque' from no-cors) which always has null body.","commonSituations":"CORS misconfiguration making the fetch fall into an opaque response; nginx buffering config for the streaming route; old in-app browsers lacking ReadableStream responses.","solutions":["Log res.type — 'opaque' means a CORS/no-cors problem; fix the CORS headers or remove mode:'no-cors' from the fetch.","Ensure the streaming route disables proxy buffering (nginx: proxy_buffering off; or X-Accel-Buffering: no response header).","Verify the backend always returns a streaming body for this route, including on its 'immediate reply' path.","Test in a modern browser to rule out missing resp.body support."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"const streamCapable = typeof ReadableStream !== 'undefined' && 'body' in Response.prototype;\nif (!streamCapable) {\n  throw new Error('当前环境不支持流式响应');\n}\n// ensure fetch is same-origin/cors, never mode:'no-cors'\nconst resp = await fetch(url, { method: 'POST', headers, body }); // no mode:'no-cors' anywhere","typeGuard":"function hasReadableBody(res: Response): res is Response & { body: NonNullable<Response['body']> } {\n  return res.body != null;\n}","tryCatchPattern":"const reader = hasReadableBody(res) ? res.body.getReader() : null;\nif (!reader) {\n  if (res.type === 'opaque') showError('CORS 配置错误导致无法读取响应流');\n  else showError('当前浏览器不支持流式响应');\n  return;\n}","preventionTips":["Never use mode:'no-cors' for streaming endpoints — opaque responses always have null body.","Configure CORS headers properly on the backend instead of bypassing them.","Disable proxy buffering on the streaming route so the body isn't closed early."],"tags":["streaming","readablestream","cors","proxy-config","vue"],"backgroundTag":null,"analyzedSha":"606a07d341a47be773fab7f4b71177f53f96b2c3","analyzedAt":"2026-08-14T22:57:27.446Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}