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(', '));
+}