denoland/deno · error · TypeError

Body is unusable

Error message

Body is unusable

What it means

After accepting the Response, Deno inspects its body: if a body exists (innerResp.body !== null) and body.unusable() returns true, the response is rejected. A body becomes unusable when its stream is already disturbed/locked/closed - i.e. the ReadableStream was read, cancelled, teed, or locked by a getReader() call before respondWith got it.

Source

Thrown at ext/http/01_http.js:225

      // that don't carry the internal slot (e.g. `Object.create(Response.prototype)`
      // or a polyfilled/foreign-realm Response). Reject those here instead of
      // crashing later on `innerResp.body`. Mirrors the Deno.serve guard
      // added in #34416.
      const innerResp = toInnerResponse(resp);
      if (innerResp === undefined) {
        throw new TypeError(
          "First argument to 'respondWith' must be a Response constructed via the Response constructor in this realm",
        );
      }

      // If response body length is known, it will be sent synchronously in a
      // single op, in other case a "response body" resource will be created and
      // we'll be streaming it.
      /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
      let respBody = null;
      if (innerResp.body !== null) {
        if (innerResp.body.unusable()) {
          throw new TypeError("Body is unusable");
        }
        if (
          ObjectPrototypeIsPrototypeOf(
            ReadableStreamPrototype,
            innerResp.body.streamOrStatic,
          )
        ) {
          if (
            innerResp.body.length === null ||
            ObjectPrototypeIsPrototypeOf(
              BlobPrototype,
              innerResp.body.source,
            )
          ) {
            respBody = innerResp.body.stream;
          } else {
            const reader = innerResp.body.stream.getReader();
            const r1 = await reader.read();

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Do not read the body before responding; respond with the original Response untouched.
  2. If you must inspect the body, clone first: const copy = response.clone(); log(await copy.text()); httpConn.respondWith(response).
  3. Cache the body payload (string/ArrayBuffer) and construct a fresh new Response per request instead of caching the Response object.
  4. Ensure only one respondWith/reader ever consumes a given response body (guard against double-fire error handlers).

Example fix

// before
const res = new Response(data);
console.log(await res.text()); // body now disturbed
httpConn.respondWith(res);

// after
const res = new Response(data);
const copy = res.clone();
console.log(await copy.text());
httpConn.respondWith(res);
Defensive patterns

Strategy: type-guard

Validate before calling

function isUsableResponse(resp: Response): boolean { return resp.body === null || !resp.body.locked; }

Type guard

function isSendableResponse(r: unknown): r is Response { return r instanceof Response && (r.body === null || !r.body.locked); }

Try / catch

try { await httpConn.respondWith(resp); } catch (e) { if (e instanceof TypeError && e.message === "Body is unusable") { await httpConn.respondWith(new Response("body consumed", { status: 500 })); return; } throw e; }

Prevention

When it happens

Trigger: Calling response.json()/text() (or cloning then reading one clone) before httpConn.respondWith(response); locking the body via response.body.getReader() for logging/teeing; returning the same Response object twice (e.g. a cached Response reused across requests); reading the body of a stream-backed Response built from a consumed stream.

Common situations: Access-logging middleware that peeks at bodies; caching a single Response instance and replaying it; race between a background reader and the responder; double-respond in error paths (respondWith the same object after a timeout also responded).

Related errors


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