Repository: denoland/deno
Pull request: fix(ext/node): implement http.res.setHeaders

Closes #30540

Also fixes res.writeHead implementation to parse array values correctly. For example, `res.writeHead(200, [["foo", "bar"]])` and `res.writeHead(200, ["foo", "bar"])` should produce the same result.

These changes allows [parallel/test-http-response-setheaders.js](https://github.com/nodejs/node/blob/v24.2.0/test/parallel/test-http-response-setheaders.js) to pass.

Changed files:
filename: ext/node/polyfills/http.ts
patch: @@ -1584,6 +1584,49 @@ ServerResponse.prototype.setHeader = function (
   return this;
 };
 
+ServerResponse.prototype.setHeaders = function setHeaders(
+  this: ServerResponse,
+  headers: Headers | Map<string, string | string[]>,
+) {
+  if (this._header) {
+    throw new ERR_HTTP_HEADERS_SENT("set");
+  }
+
+  if (
+    !headers ||
+    ArrayIsArray(headers) ||
+    typeof headers.keys !== "function" ||
+    typeof headers.get !== "function"
+  ) {
+    throw new ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);
+  }
+
+  // Headers object joins multiple cookies with a comma when using
+  // the getter to retrieve the value,
+  // unless iterating over the headers directly.
+  // We also cannot safely split by comma.
+  // To avoid setHeader overwriting the previous value we push
+  // set-cookie values in array and set them all at once.
+  const cookies = [];
+
+  for (const { 0: key, 1: value } of headers) {
+    if (key === "set-cookie") {
+      if (ArrayIsArray(value)) {
+        cookies.push(...value);
+      } else {
+        cookies.push(value);
+      }
+      continue;
+    }
+    this.setHeader(key, value);
+  }
+  if (cookies.length) {
+    this.setHeader("set-cookie", cookies);
+  }
+
+  return this;
+};
+
 ServerResponse.prototype.appendHeader = function (
   this: ServerResponse,
   name: string,
@@ -1645,10 +1688,12 @@ ServerResponse.prototype.writeHead = function (
   statusMessageOrHeaders?:
     | string
     | Record<string, string | number | string[]>
-    | Array<[string, string]>,
+    | Array<[string, string]>
+    | Array<string>,
   maybeHeaders?:
     | Record<string, string | number | string[]>
-    | Array<[string, string]>,
+    | Array<[string, string]>
+    | Array<string>,
 ) {
   this.statusCode = status;
 
@@ -1664,9 +1709,36 @@ ServerResponse.prototype.writeHead = function (
 
   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]);
+      headers = headers as Array<[string, string]> | Array<string>;
+
+      // Headers should override previous headers but still
+      // allow explicit duplicates. To do so, we first remove any
+      // existing conflicts, then use appendHeader.
+
+      if (ArrayIsArray(headers[0])) {
+        headers = headers as Array<[string, string]>;
+        for (let i = 0; i < headers.length; i++) {
+          const headerTuple = headers[i];
+          const k = headerTuple[0];
+          if (k) this.removeHeader(k);
+        }
+
+        for (let i = 0; i < headers.length; i++) {
+          const headerTuple = headers[i];
+          const k = headerTuple[0];
+          if (k) this.appendHeader(k, headerTuple[1]);
+        }
+      } else {
+        headers = headers as Array<string>;
+        for (let i = 0; i < headers.length; i += 2) {
+          const k = headers[i];
+          this.removeHeader(k);
+        }
+
+        for (let i = 0; i < headers.length; i += 2) {
+          const k = headers[i];
+          if (k) this.appendHeader(k, headers[i + 1]);
+        }
       }
     } else {
       headers = headers as Record<string, string>;

filename: tests/node_compat/config.toml
patch: @@ -591,6 +591,7 @@
 "parallel/test-http-res-write-end-dont-take-array.js" = {}
 "parallel/test-http-response-multiheaders.js" = {}
 "parallel/test-http-response-readable.js" = {}
+"parallel/test-http-response-setheaders.js" = {}
 "parallel/test-http-response-writehead-returns-this.js" = {}
 "parallel/test-http-server-delete-parser.js" = {}
 "parallel/test-http-server-method.query.js" = {}

filename: tests/unit_node/http_test.ts
patch: @@ -227,7 +227,7 @@ Deno.test("[node/http] .writeHead()", async (t) => {
     );
   });
 
-  await t.step("send status + headers array", async () => {
+  await t.step("send status + headers nested array", async () => {
     await testWriteHead(
       (res) => res.writeHead(200, [["foo", "bar"]]),
       (res) => {
@@ -236,6 +236,16 @@ Deno.test("[node/http] .writeHead()", async (t) => {
       },
     );
   });
+
+  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