denoland/deno · error · TypeError

Cannot read headers: request closed

Error message

Cannot read headers: request closed

What it means

In Deno's fetch implementation an op-backed InnerRequest (e.g. a request object handed to a Deno.serve / Deno.serveHttp handler) materializes its header list lazily: the first access to the headers calls into the native request resource. If that resource has already been closed — the handler returned, the connection finished, the request was dropped — the native read throws and is re-thrown as TypeError "Cannot read headers: request closed".

Solutions

  1. Read the headers you need synchronously at the top of the handler, before any await
  2. Snapshot the request data ([...req.headers], url, method) into a plain object and pass that along
  3. Consume/copy the body into memory (await req.text()) if the background task also needs the body
  4. If you see this from a stored request, treat it as a lifecycle bug: capture data earlier rather than catching the error

Example fix

// before
Deno.serve((req) => {
  setTimeout(() => console.log(req.headers.get("x-id")), 10); // TypeError: request closed
  return new Response("ok");
});

// after
Deno.serve((req) => {
  const id = req.headers.get("x-id"); // read while the request is alive
  setTimeout(() => console.log(id), 10);
  return new Response("ok");
});
Defensive patterns

Strategy: try-catch

Validate before calling

// No pre-check API exists — capture instead:
function snapshotRequest(req) {
  return { method: req.method, url: req.url, headers: [...req.headers] }; // safe while alive
}

Type guard

function requestIsAlive(req) {
  try {
    req.url; // cheap lazy read that throws once the resource is closed
    return true;
  } catch {
    return false;
  }
}

Try / catch

try {
  const auth = req.headers.get("authorization");
} catch (err) {
  if (err instanceof TypeError && err.message.includes("request closed")) {
    // the request lifecycle ended; use previously captured data
  } else throw err;
}

Prevention

When it happens

Trigger: Accessing request.headers on a server-side Request inside a setTimeout/queueMicrotask that runs after the handler returned; storing req objects in a queue and reading headers later; logging headers after an await that outlives the response.

Common situations: Background-job patterns that stash the incoming request for async processing; fire-and-forget telemetry after responding; retry queues holding the original request; tests that keep the request object alive past the response.

Related errors


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

Appendix: source

Thrown at ext/fetch/23_request.js:146

    StringPrototypeStartsWith(url, "blob:")
  ) {
    blobUrlEntry = blobFromObjectUrl(url);
  }
  return {
    methodInner: method,
    get method() {
      return this.methodInner;
    },
    set method(value) {
      this.methodInner = value;
    },
    headerListInner: null,
    get headerList() {
      if (this.headerListInner === null) {
        try {
          this.headerListInner = headerList();
        } catch {
          throw new TypeError("Cannot read headers: request closed");
        }
      }
      return this.headerListInner;
    },
    set headerList(value) {
      this.headerListInner = value;
    },
    body,
    redirectMode: "follow",
    redirectCount: 0,
    urlList: [typeof url === "string" ? () => url : url],
    urlListProcessed: [],
    clientRid: null,
    blobUrlEntry,
    url() {
      if (this.urlListProcessed[0] === undefined) {
        try {
          this.urlListProcessed[0] = this.urlList[0]();

View on GitHub (pinned to 9ad36f7a2c)