Skip to content

Commit 062e837

Browse files
pimterryrichardlau
authored andcommitted
http2: add support for raw header arrays in h2Stream.respond()
PR-URL: #59455 Reviewed-By: Ethan Arrowood <[email protected]> Reviewed-By: Gerhard Stöbich <[email protected]>
1 parent cf06e74 commit 062e837

File tree

5 files changed

+203
-35
lines changed

5 files changed

+203
-35
lines changed

doc/api/http2.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,10 @@ changes:
10761076
pr-url: https://github.com/nodejs/node/pull/58313
10771077
description: Following the deprecation of priority signaling as of RFC 9113,
10781078
`weight` option is deprecated.
1079+
- version:
1080+
- v22.17.0
1081+
pr-url: https://github.com/nodejs/node/pull/57917
1082+
description: Allow passing headers in raw array format.
10791083
-->
10801084

10811085
* `headers` {HTTP/2 Headers Object|Array}
@@ -1859,14 +1863,18 @@ and will throw an error.
18591863
<!-- YAML
18601864
added: v8.4.0
18611865
changes:
1866+
- version:
1867+
- REPLACEME
1868+
pr-url: https://github.com/nodejs/node/pull/59455
1869+
description: Allow passing headers in raw array format.
18621870
- version:
18631871
- v14.5.0
18641872
- v12.19.0
18651873
pr-url: https://github.com/nodejs/node/pull/33160
18661874
description: Allow explicitly setting date headers.
18671875
-->
18681876

1869-
* `headers` {HTTP/2 Headers Object}
1877+
* `headers` {HTTP/2 Headers Object|Array}
18701878
* `options` {Object}
18711879
* `endStream` {boolean} Set to `true` to indicate that the response will not
18721880
include payload data.

lib/internal/http2/core.js

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2560,8 +2560,31 @@ function callStreamClose(stream) {
25602560
stream.close();
25612561
}
25622562

2563-
function processHeaders(oldHeaders, options) {
2564-
assertIsObject(oldHeaders, 'headers');
2563+
function prepareResponseHeaders(stream, headersParam, options) {
2564+
let headers;
2565+
let statusCode;
2566+
2567+
if (ArrayIsArray(headersParam)) {
2568+
({
2569+
headers,
2570+
statusCode,
2571+
} = prepareResponseHeadersArray(headersParam, options));
2572+
stream[kRawHeaders] = headers;
2573+
} else {
2574+
({
2575+
headers,
2576+
statusCode,
2577+
} = prepareResponseHeadersObject(headersParam, options));
2578+
stream[kSentHeaders] = headers;
2579+
}
2580+
2581+
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
2582+
2583+
return { headers, headersList, statusCode };
2584+
}
2585+
2586+
function prepareResponseHeadersObject(oldHeaders, options) {
2587+
assertIsObject(oldHeaders, 'headers', ['Object', 'Array']);
25652588
const headers = { __proto__: null };
25662589

25672590
if (oldHeaders !== null && oldHeaders !== undefined) {
@@ -2582,23 +2605,58 @@ function processHeaders(oldHeaders, options) {
25822605
headers[HTTP2_HEADER_DATE] ??= utcDate();
25832606
}
25842607

2608+
validatePreparedResponseHeaders(headers, statusCode);
2609+
2610+
return {
2611+
headers,
2612+
statusCode: headers[HTTP2_HEADER_STATUS],
2613+
};
2614+
}
2615+
2616+
function prepareResponseHeadersArray(headers, options) {
2617+
let statusCode;
2618+
let isDateSet = false;
2619+
2620+
for (let i = 0; i < headers.length; i += 2) {
2621+
const header = headers[i].toLowerCase();
2622+
const value = headers[i + 1];
2623+
2624+
if (header === HTTP2_HEADER_STATUS) {
2625+
statusCode = value | 0;
2626+
} else if (header === HTTP2_HEADER_DATE) {
2627+
isDateSet = true;
2628+
}
2629+
}
2630+
2631+
if (!statusCode) {
2632+
statusCode = HTTP_STATUS_OK;
2633+
headers.unshift(HTTP2_HEADER_STATUS, statusCode);
2634+
}
2635+
2636+
if (!isDateSet && (options.sendDate == null || options.sendDate)) {
2637+
headers.push(HTTP2_HEADER_DATE, utcDate());
2638+
}
2639+
2640+
validatePreparedResponseHeaders(headers, statusCode);
2641+
2642+
return { headers, statusCode };
2643+
}
2644+
2645+
function validatePreparedResponseHeaders(headers, statusCode) {
25852646
// This is intentionally stricter than the HTTP/1 implementation, which
25862647
// allows values between 100 and 999 (inclusive) in order to allow for
25872648
// backwards compatibility with non-spec compliant code. With HTTP/2,
25882649
// we have the opportunity to start fresh with stricter spec compliance.
25892650
// This will have an impact on the compatibility layer for anyone using
25902651
// non-standard, non-compliant status codes.
25912652
if (statusCode < 200 || statusCode > 599)
2592-
throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]);
2653+
throw new ERR_HTTP2_STATUS_INVALID(statusCode);
25932654

25942655
const neverIndex = headers[kSensitiveHeaders];
25952656
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
25962657
throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex);
2597-
2598-
return headers;
25992658
}
26002659

2601-
26022660
function onFileUnpipe() {
26032661
const stream = this.sink[kOwner];
26042662
if (stream.ownsFd)
@@ -2901,7 +2959,7 @@ class ServerHttp2Stream extends Http2Stream {
29012959
}
29022960

29032961
// Initiate a response on this Http2Stream
2904-
respond(headers, options) {
2962+
respond(headersParam, options) {
29052963
if (this.destroyed || this.closed)
29062964
throw new ERR_HTTP2_INVALID_STREAM();
29072965
if (this.headersSent)
@@ -2926,15 +2984,16 @@ class ServerHttp2Stream extends Http2Stream {
29262984
state.flags |= STREAM_FLAGS_HAS_TRAILERS;
29272985
}
29282986

2929-
headers = processHeaders(headers, options);
2930-
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
2931-
this[kSentHeaders] = headers;
2987+
const {
2988+
headers,
2989+
headersList,
2990+
statusCode,
2991+
} = prepareResponseHeaders(this, headersParam, options);
29322992

29332993
state.flags |= STREAM_FLAGS_HEADERS_SENT;
29342994

29352995
// Close the writable side if the endStream option is set or status
29362996
// is one of known codes with no payload, or it's a head request
2937-
const statusCode = headers[HTTP2_HEADER_STATUS] | 0;
29382997
if (!!options.endStream ||
29392998
statusCode === HTTP_STATUS_NO_CONTENT ||
29402999
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -2964,7 +3023,7 @@ class ServerHttp2Stream extends Http2Stream {
29643023
// regular file, here the fd is passed directly. If the underlying
29653024
// mechanism is not able to read from the fd, then the stream will be
29663025
// reset with an error code.
2967-
respondWithFD(fd, headers, options) {
3026+
respondWithFD(fd, headersParam, options) {
29683027
if (this.destroyed || this.closed)
29693028
throw new ERR_HTTP2_INVALID_STREAM();
29703029
if (this.headersSent)
@@ -3001,8 +3060,11 @@ class ServerHttp2Stream extends Http2Stream {
30013060
this[kUpdateTimer]();
30023061
this.ownsFd = false;
30033062

3004-
headers = processHeaders(headers, options);
3005-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3063+
const {
3064+
headers,
3065+
statusCode,
3066+
} = prepareResponseHeadersObject(headersParam, options);
3067+
30063068
// Payload/DATA frames are not permitted in these cases
30073069
if (statusCode === HTTP_STATUS_NO_CONTENT ||
30083070
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -3030,7 +3092,7 @@ class ServerHttp2Stream extends Http2Stream {
30303092
// giving the user an opportunity to verify the details and set additional
30313093
// headers. If statCheck returns false, the operation is aborted and no
30323094
// file details are sent.
3033-
respondWithFile(path, headers, options) {
3095+
respondWithFile(path, headersParam, options) {
30343096
if (this.destroyed || this.closed)
30353097
throw new ERR_HTTP2_INVALID_STREAM();
30363098
if (this.headersSent)
@@ -3061,8 +3123,11 @@ class ServerHttp2Stream extends Http2Stream {
30613123
this[kUpdateTimer]();
30623124
this.ownsFd = true;
30633125

3064-
headers = processHeaders(headers, options);
3065-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3126+
const {
3127+
headers,
3128+
statusCode,
3129+
} = prepareResponseHeadersObject(headersParam, options);
3130+
30663131
// Payload/DATA frames are not permitted in these cases
30673132
if (statusCode === HTTP_STATUS_NO_CONTENT ||
30683133
statusCode === HTTP_STATUS_RESET_CONTENT ||

lib/internal/http2/util.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,6 @@ function prepareRequestHeadersArray(headers, session) {
689689
const headersList = buildNgHeaderString(
690690
rawHeaders,
691691
assertValidPseudoHeader,
692-
headers[kSensitiveHeaders],
693692
);
694693

695694
return {
@@ -752,14 +751,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
752751
* raw headers ([k1, v1, k2, v2]) or a header object ({ k1: v1, k2: [v2, v3] }).
753752
*/
754753
function buildNgHeaderString(arrayOrMap,
755-
assertValuePseudoHeader = assertValidPseudoHeader,
756-
sensitiveHeaders = arrayOrMap[kSensitiveHeaders]) {
754+
assertValuePseudoHeader = assertValidPseudoHeader) {
757755
let headers = '';
758756
let pseudoHeaders = '';
759757
let count = 0;
760758

761759
const singles = new SafeSet();
762-
const neverIndex = (sensitiveHeaders || emptyArray).map((v) => v.toLowerCase());
760+
const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
761+
const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase());
763762

764763
function processHeader(key, value) {
765764
key = key.toLowerCase();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const http2 = require('http2');
8+
9+
{
10+
const server = http2.createServer();
11+
server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => {
12+
assert.deepStrictEqual(rawHeaders, [
13+
':method', 'GET',
14+
':authority', `localhost:${server.address().port}`,
15+
':scheme', 'http',
16+
':path', '/',
17+
'a', 'b',
18+
'x-foo', 'bar', // Lowercased as required for HTTP/2
19+
'a', 'c', // Duplicate header order preserved
20+
]);
21+
stream.respond([
22+
'x', '1',
23+
'x-FOO', 'bar',
24+
'x', '2',
25+
]);
26+
27+
assert.partialDeepStrictEqual(stream.sentHeaders, {
28+
'__proto__': null,
29+
':status': 200,
30+
'x': [ '1', '2' ],
31+
'x-FOO': 'bar',
32+
});
33+
34+
assert.strictEqual(typeof stream.sentHeaders.date, 'string');
35+
36+
stream.end();
37+
}));
38+
39+
40+
server.listen(0, common.mustCall(() => {
41+
const port = server.address().port;
42+
const client = http2.connect(`http://localhost:${port}`);
43+
44+
const req = client.request([
45+
'a', 'b',
46+
'x-FOO', 'bar',
47+
'a', 'c',
48+
]).end();
49+
50+
assert.deepStrictEqual(req.sentHeaders, {
51+
'__proto__': null,
52+
':path': '/',
53+
':scheme': 'http',
54+
':authority': `localhost:${server.address().port}`,
55+
':method': 'GET',
56+
'a': [ 'b', 'c' ],
57+
'x-FOO': 'bar',
58+
});
59+
60+
req.on('response', common.mustCall((_headers, _flags, rawHeaders) => {
61+
assert.strictEqual(rawHeaders.length, 10);
62+
assert.deepStrictEqual(rawHeaders.slice(0, 8), [
63+
':status', '200',
64+
'x', '1',
65+
'x-foo', 'bar', // Lowercased as required for HTTP/2
66+
'x', '2', // Duplicate header order preserved
67+
]);
68+
69+
assert.strictEqual(rawHeaders[8], 'date');
70+
assert.strictEqual(typeof rawHeaders[9], 'string');
71+
72+
client.close();
73+
server.close();
74+
}));
75+
}));
76+
}

test/parallel/test-http2-raw-headers.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,33 @@ const http2 = require('http2');
88

99
{
1010
const server = http2.createServer();
11-
server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => {
11+
server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => {
1212
assert.deepStrictEqual(rawHeaders, [
1313
':path', '/foobar',
1414
':scheme', 'http',
15-
':authority', `localhost:${server.address().port}`,
16-
':method', 'GET',
15+
':authority', `test.invalid:${server.address().port}`,
16+
':method', 'POST',
1717
'a', 'b',
18-
'x-foo', 'bar',
19-
'a', 'c',
18+
'x-foo', 'bar', // Lowercased as required for HTTP/2
19+
'a', 'c', // Duplicate header order preserved
20+
]);
21+
22+
stream.respond([
23+
':status', '404',
24+
'x', '1',
25+
'x-FOO', 'bar',
26+
'x', '2',
27+
'DATE', '0000',
2028
]);
21-
stream.respond({
22-
':status': 200
29+
30+
assert.deepStrictEqual(stream.sentHeaders, {
31+
'__proto__': null,
32+
':status': '404',
33+
'x': [ '1', '2' ],
34+
'x-FOO': 'bar',
35+
'DATE': '0000',
2336
});
37+
2438
stream.end();
2539
}));
2640

@@ -32,8 +46,8 @@ const http2 = require('http2');
3246
const req = client.request([
3347
':path', '/foobar',
3448
':scheme', 'http',
35-
':authority', `localhost:${server.address().port}`,
36-
':method', 'GET',
49+
':authority', `test.invalid:${server.address().port}`,
50+
':method', 'POST',
3751
'a', 'b',
3852
'x-FOO', 'bar',
3953
'a', 'c',
@@ -43,14 +57,20 @@ const http2 = require('http2');
4357
'__proto__': null,
4458
':path': '/foobar',
4559
':scheme': 'http',
46-
':authority': `localhost:${server.address().port}`,
47-
':method': 'GET',
60+
':authority': `test.invalid:${server.address().port}`,
61+
':method': 'POST',
4862
'a': [ 'b', 'c' ],
4963
'x-FOO': 'bar',
5064
});
5165

52-
req.on('response', common.mustCall((headers) => {
53-
assert.strictEqual(headers[':status'], 200);
66+
req.on('response', common.mustCall((_headers, _flags, rawHeaders) => {
67+
assert.deepStrictEqual(rawHeaders, [
68+
':status', '404',
69+
'x', '1',
70+
'x-foo', 'bar', // Lowercased as required for HTTP/2
71+
'x', '2', // Duplicate header order preserved
72+
'date', '0000', // Server doesn't automatically set its own value
73+
]);
5474
client.close();
5575
server.close();
5676
}));

0 commit comments

Comments
 (0)