Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 062e837

Browse filesBrowse files
pimterryrichardlau
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 cf06e74 commit 062e837
Copy full SHA for 062e837

File tree

Expand file treeCollapse file tree

5 files changed

+203
-35
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+203
-35
lines changed
Open diff view settings
Collapse file

‎doc/api/http2.md‎

Copy file name to clipboardExpand all lines: doc/api/http2.md
+9-1Lines changed: 9 additions & 1 deletion
  • Display the source diff
  • Display the rich diff
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.
Collapse file

‎lib/internal/http2/core.js‎

Copy file name to clipboardExpand all lines: lib/internal/http2/core.js
+82-17Lines 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 ||
Collapse file

‎lib/internal/http2/util.js‎

Copy file name to clipboardExpand all lines: lib/internal/http2/util.js
+3-4Lines 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();
Collapse file
+76Lines 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+
}
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-http2-raw-headers.js
+33-13Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.