denoland/deno · error · TypeError

Input request's body is unusable

Error message

Input request's body is unusable

What it means

Request constructor step 41: when no new body is supplied in init but the input Request has one, the input's body must be teed via createProxy(). A body stream that is already disturbed or locked (previously read, or currently being read) is unusable, and re-using it throws TypeError "Input request's body is unusable". A request's body can only be consumed once.

Solutions

  1. Call input.clone() BEFORE reading the body, then build the new Request from the clone
  2. Read the body once into memory (await req.text()) and construct fresh Requests with that string as body
  3. Never re-use a request object after awaiting any body-reading method (text/json/arrayBuffer/formData)

Example fix

// before
const src = new Request(url, { method: "POST", body: payload });
await src.text(); // log the body
const fwd = new Request(src); // throws: body unusable

// after
const src = new Request(url, { method: "POST", body: payload });
const copy = src.clone(); // tee before reading
await src.text(); // log the body
const fwd = new Request(copy); // ok
Defensive patterns

Strategy: validation

Validate before calling

if (input instanceof Request && input.bodyUsed) {
  throw new Error("Input request body already consumed — pass the payload string instead");
}
const req = new Request(input);

Type guard

function isReusableRequestBody(req: Request): boolean {
  return !req.bodyUsed && req.body !== null ? !req.body.locked : true;
}

Try / catch

try {
  const fwd = new Request(input);
} catch (err) {
  if (err instanceof TypeError && err.message.includes("body is unusable")) {
    // body already read: build a fresh request from the buffered payload
    new Request(url, { method, headers, body: bufferedPayload });
  } else throw err;
}

Prevention

When it happens

Trigger: const r = new Request(url, { method: "POST", body: data }); await r.text(); new Request(r); also re-using a request whose stream is mid-read (piped to a WritableStream) as input to a new Request.

Common situations: Peeking at a body (logging/parsing) then trying to forward the same request object; retry logic that re-wraps an already-read request; middleware that reads the body and later constructs a new Request from the original.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/addf82a81f562d19. Report an issue: GitHub.

Appendix: source

Thrown at ext/fetch/23_request.js:585

    // 37.
    if (init.body !== undefined && init.body !== null) {
      const res = extractBody(init.body);
      initBody = res.body;
      if (res.contentType !== null && !this[_headers].has("content-type")) {
        this[_headers].append("Content-Type", res.contentType);
      }
    }

    // 38.
    const inputOrInitBody = initBody ?? inputBody;

    // 40.
    let finalBody = inputOrInitBody;

    // 41.
    if (initBody === null && inputBody !== null) {
      if (input[_body] && input[_body].unusable()) {
        throw new TypeError("Input request's body is unusable");
      }
      finalBody = inputBody.createProxy();
    }

    // 42.
    request.body = finalBody;
  }

  get method() {
    webidl.assertBranded(this, RequestPrototype);
    if (this[_method]) {
      return this[_method];
    }
    this[_method] = this[_request].method;
    return this[_method];
  }

  get url() {

View on GitHub (pinned to 9ad36f7a2c)