diff --git a/doc/api/http2.md b/doc/api/http2.md index 4ee7696cfa15..e6dd6c418692 100644 --- a/doc/api/http2.md +++ b/doc/api/http2.md @@ -3718,7 +3718,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 8007b300bd1b..f0f80aa175ef 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 000000000000..dadf306da811 --- /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 09135e865d4f..c6f7bc38d04f 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(', ')); +}