Skip to content

Commit dafee05

Browse files
pimterrytargos
authored andcommitted
http2: add support for raw header arrays in h2Stream.respond()
PR-URL: #59455 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent 8dc6f5b commit dafee05

File tree

5 files changed

+204
-35
lines changed

5 files changed

+204
-35
lines changed

doc/api/http2.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,11 @@ changes:
10801080
pr-url: https://github.com/nodejs/node/pull/58313
10811081
description: Following the deprecation of priority signaling as of RFC 9113,
10821082
`weight` option is deprecated.
1083+
- version:
1084+
- v24.0.0
1085+
- v22.17.0
1086+
pr-url: https://github.com/nodejs/node/pull/57917
1087+
description: Allow passing headers in raw array format.
10831088
-->
10841089

10851090
* `headers` {HTTP/2 Headers Object|Array}
@@ -1850,14 +1855,18 @@ and will throw an error.
18501855
<!-- YAML
18511856
added: v8.4.0
18521857
changes:
1858+
- version:
1859+
- REPLACEME
1860+
pr-url: https://github.com/nodejs/node/pull/59455
1861+
description: Allow passing headers in raw array format.
18531862
- version:
18541863
- v14.5.0
18551864
- v12.19.0
18561865
pr-url: https://github.com/nodejs/node/pull/33160
18571866
description: Allow explicitly setting date headers.
18581867
-->
18591868

1860-
* `headers` {HTTP/2 Headers Object}
1869+
* `headers` {HTTP/2 Headers Object|Array}
18611870
* `options` {Object}
18621871
* `endStream` {boolean} Set to `true` to indicate that the response will not
18631872
include payload data.

lib/internal/http2/core.js

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,8 +2541,31 @@ function callStreamClose(stream) {
25412541
stream.close();
25422542
}
25432543

2544-
function processHeaders(oldHeaders, options) {
2545-
assertIsObject(oldHeaders, 'headers');
2544+
function prepareResponseHeaders(stream, headersParam, options) {
2545+
let headers;
2546+
let statusCode;
2547+
2548+
if (ArrayIsArray(headersParam)) {
2549+
({
2550+
headers,
2551+
statusCode,
2552+
} = prepareResponseHeadersArray(headersParam, options));
2553+
stream[kRawHeaders] = headers;
2554+
} else {
2555+
({
2556+
headers,
2557+
statusCode,
2558+
} = prepareResponseHeadersObject(headersParam, options));
2559+
stream[kSentHeaders] = headers;
2560+
}
2561+
2562+
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
2563+
2564+
return { headers, headersList, statusCode };
2565+
}
2566+
2567+
function prepareResponseHeadersObject(oldHeaders, options) {
2568+
assertIsObject(oldHeaders, 'headers', ['Object', 'Array']);
25462569
const headers = { __proto__: null };
25472570

25482571
if (oldHeaders !== null && oldHeaders !== undefined) {
@@ -2563,23 +2586,58 @@ function processHeaders(oldHeaders, options) {
25632586
headers[HTTP2_HEADER_DATE] ??= utcDate();
25642587
}
25652588

2589+
validatePreparedResponseHeaders(headers, statusCode);
2590+
2591+
return {
2592+
headers,
2593+
statusCode: headers[HTTP2_HEADER_STATUS],
2594+
};
2595+
}
2596+
2597+
function prepareResponseHeadersArray(headers, options) {
2598+
let statusCode;
2599+
let isDateSet = false;
2600+
2601+
for (let i = 0; i < headers.length; i += 2) {
2602+
const header = headers[i].toLowerCase();
2603+
const value = headers[i + 1];
2604+
2605+
if (header === HTTP2_HEADER_STATUS) {
2606+
statusCode = value | 0;
2607+
} else if (header === HTTP2_HEADER_DATE) {
2608+
isDateSet = true;
2609+
}
2610+
}
2611+
2612+
if (!statusCode) {
2613+
statusCode = HTTP_STATUS_OK;
2614+
headers.unshift(HTTP2_HEADER_STATUS, statusCode);
2615+
}
2616+
2617+
if (!isDateSet && (options.sendDate == null || options.sendDate)) {
2618+
headers.push(HTTP2_HEADER_DATE, utcDate());
2619+
}
2620+
2621+
validatePreparedResponseHeaders(headers, statusCode);
2622+
2623+
return { headers, statusCode };
2624+
}
2625+
2626+
function validatePreparedResponseHeaders(headers, statusCode) {
25662627
// This is intentionally stricter than the HTTP/1 implementation, which
25672628
// allows values between 100 and 999 (inclusive) in order to allow for
25682629
// backwards compatibility with non-spec compliant code. With HTTP/2,
25692630
// we have the opportunity to start fresh with stricter spec compliance.
25702631
// This will have an impact on the compatibility layer for anyone using
25712632
// non-standard, non-compliant status codes.
25722633
if (statusCode < 200 || statusCode > 599)
2573-
throw new ERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]);
2634+
throw new ERR_HTTP2_STATUS_INVALID(statusCode);
25742635

25752636
const neverIndex = headers[kSensitiveHeaders];
25762637
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
25772638
throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex);
2578-
2579-
return headers;
25802639
}
25812640

2582-
25832641
function onFileUnpipe() {
25842642
const stream = this.sink[kOwner];
25852643
if (stream.ownsFd)
@@ -2882,7 +2940,7 @@ class ServerHttp2Stream extends Http2Stream {
28822940
}
28832941

28842942
// Initiate a response on this Http2Stream
2885-
respond(headers, options) {
2943+
respond(headersParam, options) {
28862944
if (this.destroyed || this.closed)
28872945
throw new ERR_HTTP2_INVALID_STREAM();
28882946
if (this.headersSent)
@@ -2907,15 +2965,16 @@ class ServerHttp2Stream extends Http2Stream {
29072965
state.flags |= STREAM_FLAGS_HAS_TRAILERS;
29082966
}
29092967

2910-
headers = processHeaders(headers, options);
2911-
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
2912-
this[kSentHeaders] = headers;
2968+
const {
2969+
headers,
2970+
headersList,
2971+
statusCode,
2972+
} = prepareResponseHeaders(this, headersParam, options);
29132973

29142974
state.flags |= STREAM_FLAGS_HEADERS_SENT;
29152975

29162976
// Close the writable side if the endStream option is set or status
29172977
// is one of known codes with no payload, or it's a head request
2918-
const statusCode = headers[HTTP2_HEADER_STATUS] | 0;
29192978
if (!!options.endStream ||
29202979
statusCode === HTTP_STATUS_NO_CONTENT ||
29212980
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -2945,7 +3004,7 @@ class ServerHttp2Stream extends Http2Stream {
29453004
// regular file, here the fd is passed directly. If the underlying
29463005
// mechanism is not able to read from the fd, then the stream will be
29473006
// reset with an error code.
2948-
respondWithFD(fd, headers, options) {
3007+
respondWithFD(fd, headersParam, options) {
29493008
if (this.destroyed || this.closed)
29503009
throw new ERR_HTTP2_INVALID_STREAM();
29513010
if (this.headersSent)
@@ -2982,8 +3041,11 @@ class ServerHttp2Stream extends Http2Stream {
29823041
this[kUpdateTimer]();
29833042
this.ownsFd = false;
29843043

2985-
headers = processHeaders(headers, options);
2986-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3044+
const {
3045+
headers,
3046+
statusCode,
3047+
} = prepareResponseHeadersObject(headersParam, options);
3048+
29873049
// Payload/DATA frames are not permitted in these cases
29883050
if (statusCode === HTTP_STATUS_NO_CONTENT ||
29893051
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -3011,7 +3073,7 @@ class ServerHttp2Stream extends Http2Stream {
30113073
// giving the user an opportunity to verify the details and set additional
30123074
// headers. If statCheck returns false, the operation is aborted and no
30133075
// file details are sent.
3014-
respondWithFile(path, headers, options) {
3076+
respondWithFile(path, headersParam, options) {
30153077
if (this.destroyed || this.closed)
30163078
throw new ERR_HTTP2_INVALID_STREAM();
30173079
if (this.headersSent)
@@ -3042,8 +3104,11 @@ class ServerHttp2Stream extends Http2Stream {
30423104
this[kUpdateTimer]();
30433105
this.ownsFd = true;
30443106

3045-
headers = processHeaders(headers, options);
3046-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3107+
const {
3108+
headers,
3109+
statusCode,
3110+
} = prepareResponseHeadersObject(headersParam, options);
3111+
30473112
// Payload/DATA frames are not permitted in these cases
30483113
if (statusCode === HTTP_STATUS_NO_CONTENT ||
30493114
statusCode === HTTP_STATUS_RESET_CONTENT ||

lib/internal/http2/util.js

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

696695
return {
@@ -755,14 +754,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
755754
* @returns {[string, number]}
756755
*/
757756
function buildNgHeaderString(arrayOrMap,
758-
assertValuePseudoHeader = assertValidPseudoHeader,
759-
sensitiveHeaders = arrayOrMap[kSensitiveHeaders]) {
757+
assertValuePseudoHeader = assertValidPseudoHeader) {
760758
let headers = '';
761759
let pseudoHeaders = '';
762760
let count = 0;
763761

764762
const singles = new SafeSet();
765-
const neverIndex = (sensitiveHeaders || emptyArray).map((v) => v.toLowerCase());
763+
const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
764+
const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase());
766765

767766
function processHeader(key, value) {
768767
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)