denoland/deno · error · TypeError

Body already consumed.

Error message

Body already consumed.

What it means

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.

Source

Thrown at ext/fetch/22_body.js:414

    },
    text: {
      __proto__: null,
      /** @returns {Promise<string>} */
      value: function text() {
        return consumeBody(this, "text");
      },
      writable: true,
      configurable: true,
      enumerable: true,
    },
    textStream: {
      __proto__: null,
      /** @returns {ReadableStream<string>} */
      value: function textStream() {
        webidl.assertBranded(this, prototype);
        const inner = this[bodySymbol];
        if (inner !== null && inner.unusable()) {
          throw new TypeError("Body already consumed.");
        }
        if (inner === null) {
          // A null body yields an empty, already-closed stream. Per the spec
          // this is returned as-is; no decoder is set up for it.
          const emptyStream = new ReadableStream();
          readableStreamClose(emptyStream);
          return emptyStream;
        }
        return inner.stream.pipeThrough(new TextDecoderStream());
      },
      writable: true,
      configurable: true,
      enumerable: true,
    },
  };
  return ObjectDefineProperties(prototype, mixin);
}

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Decide one consumption mode per body: stream it OR buffer it, never both
  2. Check res.bodyUsed before switching to the text stream
  3. If both are needed, tee/clone the source before the first read

Example fix

// before
const reader = res.body.getReader();
const first = await reader.read();
// later: streaming text access on the same body -> TypeError

// after
const [a, b] = res.body.tee();
const first = await a.getReader().read();
// b remains usable for the text-stream path
Defensive patterns

Strategy: validation

Validate before calling

if (res.bodyUsed || res.body?.locked) {
  throw new Error("body already consumed - cannot open text stream");
}
const stream = res.textStream ?? streamBodyAsText(res); // only on an unread body

Type guard

function hasUnreadBody(res: Response | Request): boolean {
  return !res.bodyUsed && !(res.body?.locked ?? false);
}

Try / catch

try { return getTextStream(res); } catch (e) {
  if (e instanceof TypeError && e.message.startsWith("Body already consumed.")) {
    return ReadableStream.from([previouslyBufferedText]); // fall back to buffered copy
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/49b40fc44fc8d553. Report an issue: GitHub.