Skip to content

Commit 8e25d2d

Browse files
committed
http2: add support for raw header arrays in h2Stream.respond()
1 parent eed1d33 commit 8e25d2d

File tree

4 files changed

+107
-26
lines changed

4 files changed

+107
-26
lines changed

doc/api/http2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1863,7 +1863,7 @@ changes:
18631863
description: Allow explicitly setting date headers.
18641864
-->
18651865

1866-
* `headers` {HTTP/2 Headers Object}
1866+
* `headers` {HTTP/2 Headers Object|Array}
18671867
* `options` {Object}
18681868
* `endStream` {boolean} Set to `true` to indicate that the response will not
18691869
include payload data.

lib/internal/http2/core.js

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,7 +2541,30 @@ function callStreamClose(stream) {
25412541
stream.close();
25422542
}
25432543

2544-
function processHeaders(oldHeaders, options) {
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) {
25452568
assertIsObject(oldHeaders, 'headers');
25462569
const headers = { __proto__: null };
25472570

@@ -2576,9 +2599,51 @@ function processHeaders(oldHeaders, options) {
25762599
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
25772600
throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex);
25782601

2579-
return headers;
2602+
return {
2603+
headers,
2604+
statusCode: headers[HTTP2_HEADER_STATUS],
2605+
};
25802606
}
25812607

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

25832648
function onFileUnpipe() {
25842649
const stream = this.sink[kOwner];
@@ -2882,7 +2947,7 @@ class ServerHttp2Stream extends Http2Stream {
28822947
}
28832948

28842949
// Initiate a response on this Http2Stream
2885-
respond(headers, options) {
2950+
respond(headersParam, options) {
28862951
if (this.destroyed || this.closed)
28872952
throw new ERR_HTTP2_INVALID_STREAM();
28882953
if (this.headersSent)
@@ -2907,15 +2972,16 @@ class ServerHttp2Stream extends Http2Stream {
29072972
state.flags |= STREAM_FLAGS_HAS_TRAILERS;
29082973
}
29092974

2910-
headers = processHeaders(headers, options);
2911-
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
2912-
this[kSentHeaders] = headers;
2975+
const {
2976+
headers,
2977+
headersList,
2978+
statusCode,
2979+
} = prepareResponseHeaders(this, headersParam, options);
29132980

29142981
state.flags |= STREAM_FLAGS_HEADERS_SENT;
29152982

29162983
// Close the writable side if the endStream option is set or status
29172984
// is one of known codes with no payload, or it's a head request
2918-
const statusCode = headers[HTTP2_HEADER_STATUS] | 0;
29192985
if (!!options.endStream ||
29202986
statusCode === HTTP_STATUS_NO_CONTENT ||
29212987
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -2945,7 +3011,7 @@ class ServerHttp2Stream extends Http2Stream {
29453011
// regular file, here the fd is passed directly. If the underlying
29463012
// mechanism is not able to read from the fd, then the stream will be
29473013
// reset with an error code.
2948-
respondWithFD(fd, headers, options) {
3014+
respondWithFD(fd, headersParam, options) {
29493015
if (this.destroyed || this.closed)
29503016
throw new ERR_HTTP2_INVALID_STREAM();
29513017
if (this.headersSent)
@@ -2982,8 +3048,11 @@ class ServerHttp2Stream extends Http2Stream {
29823048
this[kUpdateTimer]();
29833049
this.ownsFd = false;
29843050

2985-
headers = processHeaders(headers, options);
2986-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3051+
const {
3052+
headers,
3053+
statusCode,
3054+
} = prepareResponseHeadersObject(headersParam, options);
3055+
29873056
// Payload/DATA frames are not permitted in these cases
29883057
if (statusCode === HTTP_STATUS_NO_CONTENT ||
29893058
statusCode === HTTP_STATUS_RESET_CONTENT ||
@@ -3011,7 +3080,7 @@ class ServerHttp2Stream extends Http2Stream {
30113080
// giving the user an opportunity to verify the details and set additional
30123081
// headers. If statCheck returns false, the operation is aborted and no
30133082
// file details are sent.
3014-
respondWithFile(path, headers, options) {
3083+
respondWithFile(path, headersParam, options) {
30153084
if (this.destroyed || this.closed)
30163085
throw new ERR_HTTP2_INVALID_STREAM();
30173086
if (this.headersSent)
@@ -3042,8 +3111,11 @@ class ServerHttp2Stream extends Http2Stream {
30423111
this[kUpdateTimer]();
30433112
this.ownsFd = true;
30443113

3045-
headers = processHeaders(headers, options);
3046-
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
3114+
const {
3115+
headers,
3116+
statusCode,
3117+
} = prepareResponseHeadersObject(headersParam, options);
3118+
30473119
// Payload/DATA frames are not permitted in these cases
30483120
if (statusCode === HTTP_STATUS_NO_CONTENT ||
30493121
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();

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ 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',
1515
':authority', `localhost:${server.address().port}`,
1616
':method', 'GET',
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+
stream.respond([
22+
':status', '200',
23+
'x', '1',
24+
'x-FOO', 'bar',
25+
'x', '2',
26+
'DATE', '0000'
2027
]);
21-
stream.respond({
22-
':status': 200
23-
});
2428
stream.end();
2529
}));
2630

@@ -49,8 +53,14 @@ const http2 = require('http2');
4953
'x-FOO': 'bar',
5054
});
5155

52-
req.on('response', common.mustCall((headers) => {
53-
assert.strictEqual(headers[':status'], 200);
56+
req.on('response', common.mustCall((_headers, _flags, rawHeaders) => {
57+
assert.deepStrictEqual(rawHeaders, [
58+
':status', '200',
59+
'x', '1',
60+
'x-foo', 'bar', // Lowercased as required for HTTP/2
61+
'x', '2', // Duplicate header order preserved
62+
'date', '0000' // Server doesn't automatically set its own value
63+
]);
5464
client.close();
5565
server.close();
5666
}));

0 commit comments

Comments
 (0)