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
- Check stream.locked and the source's bodyUsed before constructing: only pass fresh streams
- Use tee() when two consumers need the same bytes, or clone() the Response before any read
- 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
- tee() the stream when two consumers need the same bytes
- Never hand the same stream to two Request/Response constructors
- In middleware that rewraps bodies, check .locked and source .bodyUsed first
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
- Body already consumed
- Body already consumed.
- Request with GET/HEAD method cannot have body
- Input request's body is unusable
- Body is unusable
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/b088393050fd3dc6.
Report an issue: GitHub.