denoland/deno · error · TypeError

ReadableStream is locked or disturbed

Error message

ReadableStream is locked or disturbed

What it means

Thrown in extractBody() (ext/fetch/22_body.js) when a ReadableStream is passed as BodyInit (to fetch(), Request, or Response) but the stream is already locked (object.locked — has an active reader) or disturbed (read or piped before). A locked/disturbed stream cannot be re-transferred as a body, so the constructor refuses it up front.

Solutions

  1. Never attach a reader to a stream you intend to send as a body — pass the stream itself unread.
  2. If you must inspect, branch it: `const [a, b] = stream.tee()` — read from a, send b.
  3. For retries, buffer the body (arrayBuffer/text) so each attempt creates a fresh stream.

Example fix

// before
const reader = upstream.body.getReader(); // locks stream
await fetch(target, { method: "POST", body: upstream.body }); // throws

// after
const [inspect, forward] = upstream.body.tee();
inspect.cancel(); // or read for logging
await fetch(target, { method: "POST", body: forward });
Defensive patterns

Strategy: type-guard

Validate before calling

function usableAsStreamBody(s) {
  return s instanceof ReadableStream && !s.locked; // disturbed state must be tracked by the caller
}
if (!usableAsStreamBody(stream)) throw new Error("stream is locked or already read");

Type guard

function isSendableBodyStream(s: unknown): s is ReadableStream {
  return s instanceof ReadableStream && !s.locked && !isDisturbed(s);
}
// practical guard: also track your own reads
function isDisturbed(s: ReadableStream): boolean {
  return s.locked; // after releaseLock, track disturbance via a wrapper flag
}

Prevention

When it happens

Trigger: `const reader = stream.getReader(); fetch(url, { body: stream })`; using a stream after `for await` over it; passing the same stream to two Requests; passing a stream returned by res.body that was already read. Note: res.body of a network response counts as disturbed once read.

Common situations: Reading a response body for inspection and then trying to forward the same stream in a new fetch/Response; tee-ing incorrectly (forgetting that a reader lock persists until releaseLock); retry logic that reuses the original stream on a second attempt.

Related errors


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

Appendix: 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 9ad36f7a2c)