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
- Read the headers you need synchronously at the top of the handler, before any await
- Snapshot the request data ([...req.headers], url, method) into a plain object and pass that along
- Consume/copy the body into memory (await req.text()) if the background task also needs the body
- 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
- Read headers at the top of the handler, before the first await
- Never store the live request object for post-response work — store a plain snapshot
- Treat 'request closed' TypeErrors as lifecycle bugs, not transient failures to retry
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
- cannot read url: request closed
- Cannot read url: request closed
- Body is unusable
- Cannot change header: headers are immutable
- Input request's body is unusable
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)