Skip to content

Commit e1ae779

Browse files
inoway46aduh95
authored andcommitted
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: #67151 Assisted-by: Codex Signed-off-by: inoway46 <inoueyuya416@gmail.com> PR-URL: #67184 Reviewed-By: Tim Perry <pimterry@gmail.com>
1 parent 030c953 commit e1ae779

4 files changed

Lines changed: 66 additions & 4 deletions

File tree

‎doc/api/http2.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3722,7 +3722,7 @@ For incoming headers:
37223722
* The `:status` header is converted to `number`.
37233723
* Duplicates of `:status`, `:method`, `:authority`, `:scheme`, `:path`,
37243724
`:protocol`, `age`, `authorization`, `access-control-allow-credentials`,
3725-
`access-control-max-age`, `access-control-request-method`, `content-encoding`,
3725+
`access-control-max-age`, `access-control-request-method`,
37263726
`content-language`, `content-length`, `content-location`, `content-md5`,
37273727
`content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
37283728
`host`, `if-match`, `if-modified-since`, `if-none-match`, `if-range`,

‎lib/internal/http2/util.js‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ const {
6464
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
6565
HTTP2_HEADER_AGE,
6666
HTTP2_HEADER_AUTHORIZATION,
67-
HTTP2_HEADER_CONTENT_ENCODING,
6867
HTTP2_HEADER_CONTENT_LANGUAGE,
6968
HTTP2_HEADER_CONTENT_LENGTH,
7069
HTTP2_HEADER_CONTENT_LOCATION,
@@ -136,7 +135,6 @@ const kSingleValueFields = new SafeSet([
136135
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
137136
HTTP2_HEADER_AGE,
138137
HTTP2_HEADER_AUTHORIZATION,
139-
HTTP2_HEADER_CONTENT_ENCODING,
140138
HTTP2_HEADER_CONTENT_LANGUAGE,
141139
HTTP2_HEADER_CONTENT_LENGTH,
142140
HTTP2_HEADER_CONTENT_LOCATION,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('node:assert/strict');
7+
const http2 = require('node:http2');
8+
const { once } = require('node:events');
9+
10+
// Content-Encoding is a list (RFC 9110 section 8.4). Preserve all codings
11+
// in their original order, regardless of single-value field validation.
12+
async function test(options, value) {
13+
const values = Array.isArray(value) ? value : [value];
14+
const expectedRaw = values.flatMap((v) => ['content-encoding', v]);
15+
const expected = values.join(', ');
16+
17+
function check(headers, rawHeaders) {
18+
assert.strictEqual(headers['content-encoding'], expected);
19+
const raw = [];
20+
for (let i = 0; i < rawHeaders.length; i += 2) {
21+
if (rawHeaders[i] === 'content-encoding')
22+
raw.push(rawHeaders[i], rawHeaders[i + 1]);
23+
}
24+
assert.deepStrictEqual(raw, expectedRaw);
25+
}
26+
27+
const server = http2.createServer(options);
28+
server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => {
29+
check(headers, rawHeaders);
30+
stream.respond({ 'content-encoding': value });
31+
stream.end();
32+
}));
33+
server.listen(0);
34+
await once(server, 'listening');
35+
36+
const client = http2.connect(`http://localhost:${server.address().port}`, options);
37+
const req = client.request({ 'content-encoding': value });
38+
req.on('response', common.mustCall((headers, flags, rawHeaders) => {
39+
check(headers, rawHeaders);
40+
}));
41+
req.resume();
42+
req.end();
43+
await once(req, 'end');
44+
client.close();
45+
await once(client, 'close');
46+
await new Promise((resolve) => server.close(resolve));
47+
}
48+
49+
(async () => {
50+
for (const options of [{}, { strictSingleValueFields: true },
51+
{ strictSingleValueFields: false }]) {
52+
for (const value of ['gzip', 'gzip, br', ['gzip', 'br'],
53+
['br', 'gzip'], ['gzip', 'gzip']]) {
54+
await test(options, value);
55+
}
56+
}
57+
})().then(common.mustCall());

‎test/parallel/test-http2-util-headers-list.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ const {
219219
HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
220220
HTTP2_HEADER_AGE,
221221
HTTP2_HEADER_AUTHORIZATION,
222-
HTTP2_HEADER_CONTENT_ENCODING,
223222
HTTP2_HEADER_CONTENT_LANGUAGE,
224223
HTTP2_HEADER_CONTENT_LENGTH,
225224
HTTP2_HEADER_CONTENT_LOCATION,
@@ -267,6 +266,7 @@ const {
267266
});
268267

269268
[
269+
HTTP2_HEADER_CONTENT_ENCODING,
270270
HTTP2_HEADER_ACCEPT_CHARSET,
271271
HTTP2_HEADER_ACCEPT_ENCODING,
272272
HTTP2_HEADER_ACCEPT_LANGUAGE,
@@ -419,3 +419,10 @@ assert.strictEqual(getAuthority({
419419
assert.strictEqual(headers.age, '10');
420420
assert.strictEqual(headers['x-multi'], 'first, second');
421421
}
422+
423+
// Content-Encoding is an ordered list, including repeated content codings.
424+
for (const values of [['gzip', 'br'], ['br', 'gzip'], ['gzip', 'gzip']]) {
425+
const rawHeaders = values.flatMap((value) => ['content-encoding', value]);
426+
assert.strictEqual(toHeaderObject(rawHeaders)['content-encoding'],
427+
values.join(', '));
428+
}

0 commit comments

Comments
 (0)