denoland/deno · error · TypeError
Cannot change headers: headers are immutable
Error message
Cannot change headers: headers are immutable
What it means
Headers.prototype.delete() throws when this[_guard] == "immutable". Per the Fetch spec, the headers of a Response returned by fetch() are guarded "immutable", so every mutating method (delete/set/append) is rejected even if the name itself is valid. The name check runs first, so a valid name on a fetched response's headers always hits this guard.
Source
Thrown at ext/fetch/20_headers.js:467
name = webidl.converters["ByteString"](name, prefix, "Argument 1");
value = webidl.converters["ByteString"](value, prefix, "Argument 2");
appendHeader(this, name, value);
}
/**
* @param {string} name
*/
delete(name) {
webidl.assertBranded(this, HeadersPrototype);
const prefix = "Failed to execute 'delete' on 'Headers'";
webidl.requiredArguments(arguments.length, 1, prefix);
name = webidl.converters["ByteString"](name, prefix, "Argument 1");
if (!checkHeaderNameForHttpTokenCodePoint(name)) {
throw new TypeError(`Invalid header name: "${name}"`);
}
if (this[_guard] == "immutable") {
throw new TypeError("Cannot change headers: headers are immutable");
}
const list = headerListFromHeaders(this);
const lowerNames = ensureLowerNames(this);
const lowercaseName = byteLowerCase(name);
let writeIdx = 0;
for (let i = 0; i < lowerNames.length; i++) {
if (lowerNames[i] !== lowercaseName) {
list[writeIdx] = list[i];
lowerNames[writeIdx] = lowerNames[i];
writeIdx++;
}
}
if (writeIdx !== list.length) {
ArrayPrototypeSplice(list, writeIdx);
ArrayPrototypeSplice(lowerNames, writeIdx);
}
}View on GitHub (pinned to 89f33cbef2)
Solutions
- Copy into a fresh mutable Headers and mutate the copy: const h = new Headers(res.headers); h.delete("x-foo")
- Build a new Response with the modified headers: new Response(res.body, { status, headers: newHeaders }) and return that
- Read values from res.headers (get/has/entries are allowed) and only ever write to headers you constructed yourself
Example fix
// before
const res = await fetch(url);
res.headers.delete("x-cache"); // TypeError: immutable
// after
const res = await fetch(url);
const headers = new Headers(res.headers);
headers.delete("x-cache");
const out = new Response(res.body, { status: res.status, headers }); Defensive patterns
Strategy: fallback
Validate before calling
const mutable = new Headers(res.headers); // copies content, guard resets to "none"
mutable.delete("x-foo"); Try / catch
try { res.headers.delete(name); } catch (e) {
if (e instanceof TypeError && e.message.includes("headers are immutable")) {
const h = new Headers(res.headers); h.delete(name); return h;
}
throw e;
} Prevention
- Assume every header object you did not construct with new Headers() is read-only
- Route all header mutations through a helper that copies first, mutates the copy
- Return new Response(res.body, { headers }) instead of editing fetched responses
When it happens
Trigger: const res = await fetch(url); res.headers.delete("x-foo"); - or res.headers.delete(...) inside a proxy/middleware that tries to strip hop-by-hop headers from an upstream response.
Common situations: Middleware that strips CORS or cache headers from fetch() responses; retry wrappers that want to remove stale headers before returning; code ported from Node/http where response headers were a plain mutable map.
Related errors
- Cannot change header: headers are immutable
- Invalid header: length must be 2, but is ${header.length}
- Invalid header name: "${name}"
- Invalid header value: "${value}"
- Response body is already used
AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16).
Data as JSON: /api/errors/b7490bf2036eff9b.
Report an issue: GitHub.