{"record":{"id":"49b40fc44fc8d553","repo":"denoland/deno","slug":"body-already-consumed-49b40f","errorCode":null,"errorMessage":"Body already consumed.","messagePattern":"Body already consumed\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"ext/fetch/22_body.js","lineNumber":414,"sourceCode":"    },\n    text: {\n      __proto__: null,\n      /** @returns {Promise<string>} */\n      value: function text() {\n        return consumeBody(this, \"text\");\n      },\n      writable: true,\n      configurable: true,\n      enumerable: true,\n    },\n    textStream: {\n      __proto__: null,\n      /** @returns {ReadableStream<string>} */\n      value: function textStream() {\n        webidl.assertBranded(this, prototype);\n        const inner = this[bodySymbol];\n        if (inner !== null && inner.unusable()) {\n          throw new TypeError(\"Body already consumed.\");\n        }\n        if (inner === null) {\n          // A null body yields an empty, already-closed stream. Per the spec\n          // this is returned as-is; no decoder is set up for it.\n          const emptyStream = new ReadableStream();\n          readableStreamClose(emptyStream);\n          return emptyStream;\n        }\n        return inner.stream.pipeThrough(new TextDecoderStream());\n      },\n      writable: true,\n      configurable: true,\n      enumerable: true,\n    },\n  };\n  return ObjectDefineProperties(prototype, mixin);\n}\n","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/denoland/deno/blob/89f33cbef296a2b287f323d42de54c871fa69c77/ext/fetch/22_body.js#L396-L432","documentation":"The body-mixin textStream accessor (ext/fetch/22_body.js) returns a decoded ReadableStream<string> for a body. If the inner body is non-null and unusable() (stream locked or already read), it throws TypeError \"Body already consumed.\" - note the trailing period, which distinguishes it from the consume() variant. A null body is fine: it returns an empty, already-closed stream.","triggerScenarios":"Accessing the text stream of a Request/Response after await res.text() or after acquiring a reader on res.body; piping the body first and then requesting the text stream.","commonSituations":"Streaming handlers that conditionally fall back to full-text processing; SSE/websocket-ish code that sometimes swaps between streaming and buffered reads on the same body.","solutions":["Decide one consumption mode per body: stream it OR buffer it, never both","Check res.bodyUsed before switching to the text stream","If both are needed, tee/clone the source before the first read"],"exampleFix":"// before\nconst reader = res.body.getReader();\nconst first = await reader.read();\n// later: streaming text access on the same body -> TypeError\n\n// after\nconst [a, b] = res.body.tee();\nconst first = await a.getReader().read();\n// b remains usable for the text-stream path","handlingStrategy":"validation","validationCode":"if (res.bodyUsed || res.body?.locked) {\n  throw new Error(\"body already consumed - cannot open text stream\");\n}\nconst stream = res.textStream ?? streamBodyAsText(res); // only on an unread body","typeGuard":"function hasUnreadBody(res: Response | Request): boolean {\n  return !res.bodyUsed && !(res.body?.locked ?? false);\n}","tryCatchPattern":"try { return getTextStream(res); } catch (e) {\n  if (e instanceof TypeError && e.message.startsWith(\"Body already consumed.\")) {\n    return ReadableStream.from([previouslyBufferedText]); // fall back to buffered copy\n  }\n  throw e;\n}","preventionTips":["Pick one consumption mode per body up front: streaming or buffered","tee()/clone() before the first read if two paths may consume it","Check bodyUsed before any conditional fallback to streaming"],"tags":["fetch","body","streams","async"],"backgroundTag":null,"analyzedSha":"89f33cbef296a2b287f323d42de54c871fa69c77","analyzedAt":"2026-08-16T07:54:21.310Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}