denoland/deno · error · TypeError

ReadableStream is locked or disturbed

Error message

ReadableStream is locked or disturbed

What it means

extractBody() in ext/fetch/22_body.js runs whenever a Request/Response is constructed from a ReadableStream body. The stream must be neither locked (getReader()/pipeTo active) nor disturbed (already read); otherwise this TypeError is thrown. The source also documents a fast path that recovers the original static body when an unread stream was materialized from one (e.g. Hono's new Response(oldResponse.body, oldResponse) pattern) - but only if the stream is still untouched.

Source

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

    source = TypedArrayPrototypeSlice(object);
  } else if (isArrayBuffer(object)) {
    source = TypedArrayPrototypeSlice(new Uint8Array(object));
  } else if (ObjectPrototypeIsPrototypeOf(FormDataPrototype, object)) {
    const res = formDataToBlob(object);
    stream = res.stream();
    source = res;
    length = res.size;
    contentType = res.type;
  } else if (
    ObjectPrototypeIsPrototypeOf(URLSearchParamsPrototype, object)
  ) {
    // TODO(@satyarohith): not sure what primordial here.
    // deno-lint-ignore deno-internal/prefer-primordials
    source = object.toString();
    contentType = "application/x-www-form-urlencoded;charset=UTF-8";
  } else if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, object)) {
    if (object.locked || isReadableStreamDisturbed(object)) {
      throw new TypeError("ReadableStream is locked or disturbed");
    }
    // Fast path: this stream was materialized from a static body and has not
    // been read. A common framework pattern (e.g. Hono middleware) is to
    // reconstruct a response via `new Response(oldResponse.body, oldResponse)`
    // just to mutate headers. Without recovering the static body, the
    // reconstructed body would be served through the streaming (chunked) path,
    // losing Content-Length and the single-write fast response op. Recover the
    // original static body so the fast path is preserved.
    //
    // Only recover when the resulting length matches the original body's
    // known-length semantics: a string source's byte length is genuinely known
    // (just deferred to avoid an eager encode), and a Uint8Array source is only
    // known-length if `staticBodyLength` was recorded for it. Recovering a
    // Uint8Array whose length was *unknown* (e.g. a chunked request body the
    // server buffered) would wrongly synthesize a Content-Length when the body
    // is later sent, so leave those as a stream.
    const recoveredSource = WeakMapPrototypeGet(staticBodySource, object);
    const knownLength = WeakMapPrototypeGet(staticBodyLength, object);

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Check stream.locked and the source's bodyUsed before constructing: only pass fresh streams
  2. Use tee() when two consumers need the same bytes, or clone() the Response before any read
  3. Consume once into a Uint8Array/string and build new Response objects from the buffer for repeats

Example fix

// before
const reader = req.body.getReader();
await reader.read();
return new Response(req.body); // TypeError: locked or disturbed

// after
const [a, b] = req.body.tee();
await a.getReader().read();
return new Response(b); // b is still usable
Defensive patterns

Strategy: validation

Validate before calling

function usableStream(stream: ReadableStream): boolean {
  return !stream.locked; // disturbed streams must be avoided by design: never reuse a read stream
}
if (bodyStream instanceof ReadableStream && usableStream(bodyStream)) {
  const res = new Response(bodyStream);
}

Type guard

function isFreshReadableStream(s: unknown): s is ReadableStream<Uint8Array> {
  return s instanceof ReadableStream && !s.locked;
}

Try / catch

try { return new Response(bodyStream); } catch (e) {
  if (e instanceof TypeError && e.message.includes("locked or disturbed")) {
    throw new Error("body stream already used - tee() or clone() before first read");
  }
  throw e;
}

Prevention

When it happens

Trigger: const r = req.body.getReader(); new Response(req.body) after reading; passing a stream that a previous pipeThrough/pipeTo locked; re-wrapping a response body in middleware after it was already consumed.

Common situations: Framework middleware that re-creates responses (Hono-style) after touching the body; retry/tee logic that reuses a stream; passing the same stream to two constructors expecting independent reads.

Related errors


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