denoland/deno · error · TypeError

ReadableStream is locked

Error message

ReadableStream is locked

What it means

On the streaming-response path, when the response body is backed by an op resource (getReadableStreamResourceBacking returns non-null, e.g. a file or socket-backed stream), Deno must acquire the JS-side stream lock with getReader() to hand the resource to op_http_write_resource. If respBody.locked is already true someone else holds a reader and the transfer cannot proceed.

Source

Thrown at ext/http/01_http.js:300

        ) {
          await respBody.cancel(error);
        }
        throw error;
      }

      if (isStreamingResponseBody) {
        let success = false;
        if (
          respBody === null ||
          !ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, respBody)
        ) {
          throw new TypeError("Unreachable");
        }
        const resourceBacking = getReadableStreamResourceBacking(respBody);
        let reader;
        if (resourceBacking) {
          if (respBody.locked) {
            throw new TypeError("ReadableStream is locked");
          }
          reader = respBody.getReader(); // Acquire JS lock.
          try {
            await op_http_write_resource(
              writeStreamRid,
              resourceBacking.rid,
            );
            if (resourceBacking.autoClose) core.tryClose(resourceBacking.rid);
            readableStreamClose(respBody); // Release JS lock.
            success = true;
          } catch (error) {
            const connError = httpConn[connErrorSymbol];
            if (
              ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error) &&
              connError != null
            ) {
              // deno-lint-ignore no-ex-assign
              error = new connError.constructor(connError.message);

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Don't take a reader on the body you pass to respondWith; let Deno stream it.
  2. If you need to observe the stream, read it fully into memory or tee and consume only one branch.
  3. For resource-backed streams (files, sockets), prefer returning the stream directly in new Response(stream) and avoid any intermediate reader/lock.
  4. Release locks you no longer need: reader.releaseLock() before responding (only if no read is pending).

Example fix

// before
const body = file.readable;
const reader = body.getReader(); // lock held
httpConn.respondWith(new Response(body));

// after
const body = file.readable;
httpConn.respondWith(new Response(body)); // no reader taken
Defensive patterns

Strategy: validation

Validate before calling

const body = resp.body;
if (body instanceof ReadableStream && body.locked) {
  throw new Error("response body stream is locked; recreate the stream before responding");
}

Type guard

function isUnlockedStream(s: unknown): s is ReadableStream { return s instanceof ReadableStream && !s.locked; }

Try / catch

try { await httpConn.respondWith(new Response(stream)); } catch (e) { if (e instanceof TypeError && e.message === "ReadableStream is locked") { httpConn.close(); return; } throw e; }

Prevention

When it happens

Trigger: Calling response.body.getReader() (or pipeThrough/pipeTo, or response.body.tee()) and not releasing it before respondWith; piping a resource-backed stream (from Deno.open / Deno.stdin / a socket) into the Response while a consumer still holds a reader; teeing a resource-backed stream (tee locks and reads it).

Common situations: Progress meters that attach a reader to file/socket streams; combining tee() with streaming responses; duplex piping where one side keeps the reader; re-serving a stream after a cancelled request without recreating it.

Related errors


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