Repository: denoland/deno
Pull request: fix(node/http): support all `.writeHead()` signatures

Implement the missing `.writeHead()` signatures from Node's `ServerResponse` class that we didn't support.

Fixes https://github.com/denoland/deno/issues/24468

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -3,7 +3,7 @@
 // TODO(petamoriken): enable prefer-primordials for node polyfills
 // deno-lint-ignore-file prefer-primordials
 
-import { core } from "ext:core/mod.js";
+import { core, primordials } from "ext:core/mod.js";
 import {
   op_fetch_response_upgrade,
   op_fetch_send,
@@ -68,6 +68,7 @@ import { resourceForReadableStream } from "ext:deno_web/06_streams.js";
 import { TcpConn } from "ext:deno_net/01_net.js";
 
 const { internalRidSymbol } = core;
+const { ArrayIsArray } = primordials;
 
 enum STATUS_CODES {
   /** RFC 7231, 6.2.1 */
@@ -1458,20 +1459,65 @@ export class ServerResponse extends NodeWritable {
   getHeaderNames() {
     return Object.keys(this.#headers);
   }
-  getHeaders() {
+  getHeaders(): Record<string, string | number | string[]> {
+    // @ts-ignore Ignore null __proto__
     return { __proto__: null, ...this.#headers };
   }
   hasHeader(name: string) {
     return Object.hasOwn(this.#headers, name);
   }
 
-  writeHead(status: number, headers: Record<string, string> = {}) {
+  writeHead(
+    status: number,
+    statusMessage?: string,
+    headers?:
+      | Record<string, string | number | string[]>
+      | Array<[string, string]>,
+  ): this;
+  writeHead(
+    status: number,
+    headers?:
+      | Record<string, string | number | string[]>
+      | Array<[string, string]>,
+  ): this;
+  writeHead(
+    status: number,
+    statusMessageOrHeaders?:
+      | string
+      | Record<string, string | number | string[]>
+      | Array<[string, string]>,
+    maybeHeaders?:
+      | Record<string, string | number | string[]>
+      | Array<[string, string]>,
+  ): this {
     this.statusCode = status;
-    for (const k in headers) {
-      if (Object.hasOwn(headers, k)) {
-        this.setHeader(k, headers[k]);
+
+    let headers = null;
+    if (typeof statusMessageOrHeaders === "string") {
+      this.statusMessage = statusMessageOrHeaders;
+      if (maybeHeaders !== undefined) {
+        headers = maybeHeaders;
       }
+    } else if (statusMessageOrHeaders !== undefined) {
+      headers = statusMessageOrHeaders;
     }
+
+    if (headers !== null) {
+      if (ArrayIsArray(headers)) {
+        headers = headers as Array<[string, string]>;
+        for (let i = 0; i < headers.length; i++) {
+          this.appendHeader(headers[i][0], headers[i][1]);
+        }
+      } else {
+        headers = headers as Record<string, string>;
+        for (const k in headers) {
+          if (Object.hasOwn(headers, k)) {
+            this.setHeader(k, headers[k]);
+          }
+        }
+      }
+    }
+
     return this;
   }
 

filename: tests/unit_node/http_test.ts
patch: @@ -1,7 +1,7 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 
 import EventEmitter from "node:events";
-import http, { type RequestOptions } from "node:http";
+import http, { type RequestOptions, type ServerResponse } from "node:http";
 import url from "node:url";
 import https from "node:https";
 import net from "node:net";
@@ -142,6 +142,93 @@ Deno.test("[node/http] chunked response", async () => {
   }
 });
 
+Deno.test("[node/http] .writeHead()", async (t) => {
+  async function testWriteHead(
+    onRequest: (res: ServerResponse) => void,
+    onResponse: (res: Response) => void,
+  ) {
+    const { promise, resolve } = Promise.withResolvers<void>();
+    const server = http.createServer((_req, res) => {
+      onRequest(res);
+      res.end();
+    });
+    server.listen(async () => {
+      const res = await fetch(
+        // deno-lint-ignore no-explicit-any
+        `http://127.0.0.1:${(server.address() as any).port}/`,
+      );
+      await res.body?.cancel();
+
+      onResponse(res);
+
+      server.close(() => resolve());
+    });
+
+    await promise;
+  }
+
+  await t.step("send status code", async () => {
+    await testWriteHead(
+      (res) => res.writeHead(404),
+      (res) => {
+        assertEquals(res.status, 404);
+      },
+    );
+  });
+
+  // TODO(@marvinhagemeister): hyper doesn't support custom status text
+  // await t.step("send status + custom status text", async () => {
+  //   await testWriteHead(
+  //     (res) => res.writeHead(404, "some text"),
+  //     (res) => {
+  //       assertEquals(res.status, 404);
+  //       assertEquals(res.statusText, "some text");
+  //     },
+  //   );
+  // });
+
+  await t.step("send status + custom status text + headers obj", async () => {
+    await testWriteHead(
+      (res) => res.writeHead(404, "some text", { foo: "bar" }),
+      (res) => {
+        assertEquals(res.status, 404);
+        // TODO(@marvinhagemeister): hyper doesn't support custom
+        // status text
+        // assertEquals(res.statusText, "some text");
+        assertEquals(res.headers.get("foo"), "bar");
+      },
+    );
+  });
+
+  await t.step("send status + headers obj", async () => {
+    await testWriteHead(
+      (res) => {
+        res.writeHead(200, {
+          foo: "bar",
+          bar: ["foo1", "foo2"],
+          foobar: 1,
+        });
+      },
+      (res) => {
+        assertEquals(res.status, 200);
+        assertEquals(res.headers.get("foo"), "bar");
+        assertEquals(res.headers.get("bar"), "foo1, foo2");
+        assertEquals(res.headers.get("foobar"), "1");
+      },
+    );
+  });
+
+  await t.step("send status + headers array", async () => {
+    await testWriteHead(
+      (res) => res.writeHead(200, [["foo", "bar"]]),
+      (res) => {
+        assertEquals(res.status, 200);
+        assertEquals(res.headers.get("foo"), "bar");
+      },
+    );
+  });
+});
+
 // Test empty chunks: https://github.com/denoland/deno/issues/17194
 Deno.test("[node/http] empty chunk in the middle of response", async () => {
   const { promise, resolve } = Promise.withResolvers<void>();