denoland/deno · error · TypeError

Cannot change headers: headers are immutable

Error message

Cannot change headers: headers are immutable

What it means

Thrown by Headers.delete() when the Headers object has guard "immutable". In Deno, headers become immutable when they come from a network fetch Response (26_fetch.js wraps responses with guard "immutable") and from redirected/preserved responses. The guard check happens after name validation but before any mutation, so every delete() on such headers throws.

Solutions

  1. Copy into a fresh mutable Headers: `const headers = new Headers(res.headers); headers.delete("set-cookie");` then build `new Response(res.body, { status: res.status, headers })`.
  2. Construct `new Response(res.body, res)` and mutate that response's headers (new Response headers have guard "none"... "response" which allows mutation).
  3. If you only need to read, use res.headers.get() — reads never throw this.

Example fix

// before
const res = await fetch(upstream);
res.headers.delete("server"); // TypeError: immutable

// after
const res = await fetch(upstream);
const headers = new Headers(res.headers);
headers.delete("server");
return new Response(res.body, { status: res.status, headers });
Defensive patterns

Strategy: fallback

Try / catch

try { res.headers.delete(name); } catch (e) { if (e instanceof TypeError && e.message.includes("immutable")) { /* fall back to copy */ } else throw e; }

Prevention

When it happens

Trigger: `const res = await fetch(url); res.headers.delete("set-cookie")`. Also headers of a Response created via fromInnerResponse with "immutable" guard (redirect handling, op_http responses preserved across restarts).

Common situations: Middleware that wants to strip hop-by-hop headers (set-cookie, server) from a fetched upstream response before returning it; proxy code that reuses res.headers directly.

Related errors


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

Appendix: 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 9ad36f7a2c)