Repository: denoland/deno
Pull request: fix(node/http): normalize header names in `ServerResponse`

Fixes https://github.com/denoland/deno/issues/26115.

We weren't normalizing the headers to lower case, so code that attempted to delete the `Content-Length` header (but used a different case) wasn't actually removing the header.

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -72,7 +72,7 @@ import { STATUS_CODES } from "node:_http_server";
 import { methods as METHODS } from "node:_http_common";
 
 const { internalRidSymbol } = core;
-const { ArrayIsArray } = primordials;
+const { ArrayIsArray, StringPrototypeToLowerCase } = primordials;
 
 type Chunk = string | Buffer | Uint8Array;
 
@@ -1270,20 +1270,21 @@ export class ServerResponse extends NodeWritable {
     if (Array.isArray(value)) {
       this.#hasNonStringHeaders = true;
     }
-    this.#headers[name] = value;
+    this.#headers[StringPrototypeToLowerCase(name)] = value;
     return this;
   }
 
   appendHeader(name: string, value: string | string[]) {
-    if (this.#headers[name] === undefined) {
+    const key = StringPrototypeToLowerCase(name);
+    if (this.#headers[key] === undefined) {
       if (Array.isArray(value)) this.#hasNonStringHeaders = true;
-      this.#headers[name] = value;
+      this.#headers[key] = value;
     } else {
       this.#hasNonStringHeaders = true;
-      if (!Array.isArray(this.#headers[name])) {
-        this.#headers[name] = [this.#headers[name]];
+      if (!Array.isArray(this.#headers[key])) {
+        this.#headers[key] = [this.#headers[key]];
       }
-      const header = this.#headers[name];
+      const header = this.#headers[key];
       if (Array.isArray(value)) {
         header.push(...value);
       } else {
@@ -1294,10 +1295,10 @@ export class ServerResponse extends NodeWritable {
   }
 
   getHeader(name: string) {
-    return this.#headers[name];
+    return this.#headers[StringPrototypeToLowerCase(name)];
   }
   removeHeader(name: string) {
-    delete this.#headers[name];
+    delete this.#headers[StringPrototypeToLowerCase(name)];
   }
   getHeaderNames() {
     return Object.keys(this.#headers);

filename: tests/unit_node/http_test.ts
patch: @@ -1147,6 +1147,34 @@ Deno.test("[node/http] ServerResponse appendHeader set-cookie", async () => {
   await promise;
 });
 
+Deno.test("[node/http] ServerResponse header names case insensitive", async () => {
+  const { promise, resolve } = Promise.withResolvers<void>();
+  const server = http.createServer((_req, res) => {
+    res.setHeader("Content-Length", "12345");
+    res.removeHeader("content-length");
+    assertEquals(res.getHeader("Content-Length"), undefined);
+    assert(!res.hasHeader("Content-Length"));
+    res.appendHeader("content-length", "12345");
+    res.removeHeader("Content-Length");
+    assertEquals(res.getHeader("content-length"), undefined);
+    assert(!res.hasHeader("content-length"));
+    res.end("Hello World");
+  });
+
+  server.listen(async () => {
+    const { port } = server.address() as { port: number };
+    const res = await fetch(`http://localhost:${port}`);
+    assertEquals(res.headers.get("Content-Length"), null);
+    assertEquals(res.headers.get("content-length"), null);
+    assertEquals(await res.text(), "Hello World");
+    server.close(() => {
+      resolve();
+    });
+  });
+
+  await promise;
+});
+
 Deno.test("[node/http] IncomingMessage override", () => {
   const req = new http.IncomingMessage(new net.Socket());
   // https://github.com/dougmoscrop/serverless-http/blob/3aaa6d0fe241109a8752efb011c242d249f32368/lib/request.js#L20-L30