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

  1. Copy into a fresh mutable Headers and mutate the copy: const h = new Headers(res.headers); h.delete("x-foo")
  2. Build a new Response with the modified headers: new Response(res.body, { status, headers: newHeaders }) and return that
  3. 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

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


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