Commit b48b454e037 for nodejs

commit b48b454e037643e1e2eb51a2dc26f5b45b4700a4
Author: Yuya Inoue <65857152+inoway46@users.noreply.github.com>
Date:   Sun Nov 8 21:21:12 2026 +0900

    http2: preserve multiple content-encoding values

    Remove content-encoding from the single-value field set so incoming
    values are joined in order and outgoing arrays are accepted.

    Update the header documentation and add coverage for requests and
    responses, including repeated codings and strict validation options.

    For repeated fields, the incoming value now includes every coding
    instead of only the first. Outgoing arrays no longer throw
    ERR_HTTP2_HEADER_SINGLE_VALUE by default.

    Fixes: https://github.com/nodejs/node/issues/67151
    Assisted-by: Codex
    Signed-off-by: inoway46 <inoueyuya416@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/67184
    Reviewed-By: Tim Perry <pimterry@gmail.com>

diff --git a/doc/api/http2.md b/doc/api/http2.md
index ac97dd6dbfa..def3c66d7c5 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -3722,7 +3722,7 @@ For incoming headers:
 * The `:status` header is converted to `number`.
 * Duplicates of `:status`, `:method`, `:authority`, `:scheme`, `:path`,
   `:protocol`, `age`, `authorization`, `access-control-allow-credentials`,
-  `access-control-max-age`, `access-control-request-method`, `content-encoding`,
+  `access-control-max-age`, `access-control-request-method`,
   `content-language`, `content-length`, `content-location`, `content-md5`,
   `content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
   `host`, `if-match`, `if-modified-since`, `if-none-match`, `if-range`,
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index 8007b300bd1..f0f80aa175e 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -64,7 +64,6 @@ const {
   HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
   HTTP2_HEADER_AGE,
   HTTP2_HEADER_AUTHORIZATION,
-  HTTP2_HEADER_CONTENT_ENCODING,
   HTTP2_HEADER_CONTENT_LANGUAGE,
   HTTP2_HEADER_CONTENT_LENGTH,
   HTTP2_HEADER_CONTENT_LOCATION,
@@ -136,7 +135,6 @@ const kSingleValueFields = new SafeSet([
   HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
   HTTP2_HEADER_AGE,
   HTTP2_HEADER_AUTHORIZATION,
-  HTTP2_HEADER_CONTENT_ENCODING,
   HTTP2_HEADER_CONTENT_LANGUAGE,
   HTTP2_HEADER_CONTENT_LENGTH,
   HTTP2_HEADER_CONTENT_LOCATION,
diff --git a/test/parallel/test-http2-content-encoding.js b/test/parallel/test-http2-content-encoding.js
new file mode 100644
index 00000000000..dadf306da81
--- /dev/null
+++ b/test/parallel/test-http2-content-encoding.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+const assert = require('node:assert/strict');
+const http2 = require('node:http2');
+const { once } = require('node:events');
+
+// Content-Encoding is a list (RFC 9110 section 8.4). Preserve all codings
+// in their original order, regardless of single-value field validation.
+async function test(options, value) {
+  const values = Array.isArray(value) ? value : [value];
+  const expectedRaw = values.flatMap((v) => ['content-encoding', v]);
+  const expected = values.join(', ');
+
+  function check(headers, rawHeaders) {
+    assert.strictEqual(headers['content-encoding'], expected);
+    const raw = [];
+    for (let i = 0; i < rawHeaders.length; i += 2) {
+      if (rawHeaders[i] === 'content-encoding')
+        raw.push(rawHeaders[i], rawHeaders[i + 1]);
+    }
+    assert.deepStrictEqual(raw, expectedRaw);
+  }
+
+  const server = http2.createServer(options);
+  server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => {
+    check(headers, rawHeaders);
+    stream.respond({ 'content-encoding': value });
+    stream.end();
+  }));
+  server.listen(0);
+  await once(server, 'listening');
+
+  const client = http2.connect(`http://localhost:${server.address().port}`, options);
+  const req = client.request({ 'content-encoding': value });
+  req.on('response', common.mustCall((headers, flags, rawHeaders) => {
+    check(headers, rawHeaders);
+  }));
+  req.resume();
+  req.end();
+  await once(req, 'end');
+  client.close();
+  await once(client, 'close');
+  await new Promise((resolve) => server.close(resolve));
+}
+
+(async () => {
+  for (const options of [{}, { strictSingleValueFields: true },
+                         { strictSingleValueFields: false }]) {
+    for (const value of ['gzip', 'gzip, br', ['gzip', 'br'],
+                         ['br', 'gzip'], ['gzip', 'gzip']]) {
+      await test(options, value);
+    }
+  }
+})().then(common.mustCall());
diff --git a/test/parallel/test-http2-util-headers-list.js b/test/parallel/test-http2-util-headers-list.js
index 09135e865d4..c6f7bc38d04 100644
--- a/test/parallel/test-http2-util-headers-list.js
+++ b/test/parallel/test-http2-util-headers-list.js
@@ -219,7 +219,6 @@ const {
   HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
   HTTP2_HEADER_AGE,
   HTTP2_HEADER_AUTHORIZATION,
-  HTTP2_HEADER_CONTENT_ENCODING,
   HTTP2_HEADER_CONTENT_LANGUAGE,
   HTTP2_HEADER_CONTENT_LENGTH,
   HTTP2_HEADER_CONTENT_LOCATION,
@@ -267,6 +266,7 @@ const {
 });

 [
+  HTTP2_HEADER_CONTENT_ENCODING,
   HTTP2_HEADER_ACCEPT_CHARSET,
   HTTP2_HEADER_ACCEPT_ENCODING,
   HTTP2_HEADER_ACCEPT_LANGUAGE,
@@ -419,3 +419,10 @@ assert.strictEqual(getAuthority({
   assert.strictEqual(headers.age, '10');
   assert.strictEqual(headers['x-multi'], 'first, second');
 }
+
+// Content-Encoding is an ordered list, including repeated content codings.
+for (const values of [['gzip', 'br'], ['br', 'gzip'], ['gzip', 'gzip']]) {
+  const rawHeaders = values.flatMap((value) => ['content-encoding', value]);
+  assert.strictEqual(toHeaderObject(rawHeaders)['content-encoding'],
+                     values.join(', '));
+}