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
- Never attach a reader to a stream you intend to send as a body — pass the stream itself unread.
- If you must inspect, branch it: `const [a, b] = stream.tee()` — read from a, send b.
- 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
- Pass streams unread; never getReader() on a stream destined to be a body.
- Use stream.tee() to branch inspection and forwarding.
- Buffer bodies (text/arrayBuffer) when retries will re-send them.
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
- Body already consumed
- Body already consumed.
- Body is unusable
- Body is unusable
- Input request's body is unusable
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)