Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
http2: add support for raw header arrays in h2Stream.respond()
  • Loading branch information
pimterry committed Aug 12, 2025
commit d354e2985aaddad96de15a19c47dfeff84d32919
2 changes: 1 addition & 1 deletion doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1863,7 +1863,7 @@ changes:
description: Allow explicitly setting date headers.
-->

* `headers` {HTTP/2 Headers Object}
* `headers` {HTTP/2 Headers Object|Array}
Comment thread
Flarna marked this conversation as resolved.
* `options` {Object}
* `endStream` {boolean} Set to `true` to indicate that the response will not
include payload data.
Expand Down
98 changes: 85 additions & 13 deletions lib/internal/http2/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2541,7 +2541,30 @@ function callStreamClose(stream) {
stream.close();
}

function processHeaders(oldHeaders, options) {
function prepareResponseHeaders(stream, headersParam, options) {
let headers;
let statusCode;

if (ArrayIsArray(headersParam)) {
({
headers,
statusCode,
} = prepareResponseHeadersArray(headersParam, options));
stream[kRawHeaders] = headers;
} else {
({
headers,
statusCode,
} = prepareResponseHeadersObject(headersParam, options));
stream[kSentHeaders] = headers;
}

const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);

return { headers, headersList, statusCode };
}

function prepareResponseHeadersObject(oldHeaders, options) {
assertIsObject(oldHeaders, 'headers');
Comment thread
pimterry marked this conversation as resolved.
Outdated
const headers = { __proto__: null };

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

return headers;
return {
headers,
statusCode: headers[HTTP2_HEADER_STATUS],
};
}

function prepareResponseHeadersArray(headers, options) {
let statusCode;
let isDateSet = false;

for (let i = 0; i < headers.length; i += 2) {
const header = headers[i].toLowerCase();
const value = headers[i + 1];

if (header === HTTP2_HEADER_STATUS) {
statusCode = value | 0;
} else if (header === HTTP2_HEADER_DATE) {
isDateSet = true;
}
}

if (!statusCode) {
statusCode = HTTP_STATUS_OK;
headers.unshift(HTTP2_HEADER_STATUS, statusCode);
}

if (!isDateSet && (options.sendDate == null || options.sendDate)) {
headers.push(HTTP2_HEADER_DATE, utcDate());
}

// This is intentionally stricter than the HTTP/1 implementation, which
Comment thread
pimterry marked this conversation as resolved.
// allows values between 100 and 999 (inclusive) in order to allow for
// backwards compatibility with non-spec compliant code. With HTTP/2,
// we have the opportunity to start fresh with stricter spec compliance.
// This will have an impact on the compatibility layer for anyone using
// non-standard, non-compliant status codes.
if (statusCode < 200 || statusCode > 599)
throw new ERR_HTTP2_STATUS_INVALID(statusCode);

const neverIndex = headers[kSensitiveHeaders];
if (neverIndex !== undefined && !ArrayIsArray(neverIndex))
throw new ERR_INVALID_ARG_VALUE('headers[http2.neverIndex]', neverIndex);

return { headers, statusCode };
}

function onFileUnpipe() {
const stream = this.sink[kOwner];
Expand Down Expand Up @@ -2882,7 +2947,7 @@ class ServerHttp2Stream extends Http2Stream {
}

// Initiate a response on this Http2Stream
respond(headers, options) {
respond(headersParam, options) {
if (this.destroyed || this.closed)
throw new ERR_HTTP2_INVALID_STREAM();
if (this.headersSent)
Expand All @@ -2907,15 +2972,16 @@ class ServerHttp2Stream extends Http2Stream {
state.flags |= STREAM_FLAGS_HAS_TRAILERS;
}

headers = processHeaders(headers, options);
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
this[kSentHeaders] = headers;
const {
headers,
headersList,
statusCode,
} = prepareResponseHeaders(this, headersParam, options);

state.flags |= STREAM_FLAGS_HEADERS_SENT;

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

headers = processHeaders(headers, options);
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
const {
headers,
statusCode,
} = prepareResponseHeadersObject(headersParam, options);

// Payload/DATA frames are not permitted in these cases
if (statusCode === HTTP_STATUS_NO_CONTENT ||
statusCode === HTTP_STATUS_RESET_CONTENT ||
Expand Down Expand Up @@ -3011,7 +3080,7 @@ class ServerHttp2Stream extends Http2Stream {
// giving the user an opportunity to verify the details and set additional
// headers. If statCheck returns false, the operation is aborted and no
// file details are sent.
respondWithFile(path, headers, options) {
respondWithFile(path, headersParam, options) {
if (this.destroyed || this.closed)
throw new ERR_HTTP2_INVALID_STREAM();
if (this.headersSent)
Expand Down Expand Up @@ -3042,8 +3111,11 @@ class ServerHttp2Stream extends Http2Stream {
this[kUpdateTimer]();
this.ownsFd = true;

headers = processHeaders(headers, options);
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
const {
headers,
statusCode,
} = prepareResponseHeadersObject(headersParam, options);

// Payload/DATA frames are not permitted in these cases
if (statusCode === HTTP_STATUS_NO_CONTENT ||
statusCode === HTTP_STATUS_RESET_CONTENT ||
Expand Down
7 changes: 3 additions & 4 deletions lib/internal/http2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,6 @@ function prepareRequestHeadersArray(headers, session) {
const headersList = buildNgHeaderString(
rawHeaders,
assertValidPseudoHeader,
headers[kSensitiveHeaders],
);

return {
Expand Down Expand Up @@ -755,14 +754,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
* @returns {[string, number]}
*/
function buildNgHeaderString(arrayOrMap,
assertValuePseudoHeader = assertValidPseudoHeader,
sensitiveHeaders = arrayOrMap[kSensitiveHeaders]) {
assertValuePseudoHeader = assertValidPseudoHeader) {
let headers = '';
let pseudoHeaders = '';
let count = 0;

const singles = new SafeSet();
const neverIndex = (sensitiveHeaders || emptyArray).map((v) => v.toLowerCase());
const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase());

function processHeader(key, value) {
key = key.toLowerCase();
Expand Down
26 changes: 18 additions & 8 deletions test/parallel/test-http2-raw-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ const http2 = require('http2');

{
const server = http2.createServer();
server.on('stream', common.mustCall((stream, headers, flags, rawHeaders) => {
server.on('stream', common.mustCall((stream, _headers, _flags, rawHeaders) => {
assert.deepStrictEqual(rawHeaders, [
':path', '/foobar',
':scheme', 'http',
':authority', `localhost:${server.address().port}`,
':method', 'GET',
'a', 'b',
'x-foo', 'bar',
'a', 'c',
'x-foo', 'bar', // Lowercased as required for HTTP/2
'a', 'c', // Duplicate header order preserved
]);
stream.respond([
':status', '200',
'x', '1',
'x-FOO', 'bar',
'x', '2',
'DATE', '0000',
]);
stream.respond({
':status': 200
});
stream.end();
}));

Expand Down Expand Up @@ -49,8 +53,14 @@ const http2 = require('http2');
'x-FOO': 'bar',
});

req.on('response', common.mustCall((headers) => {
assert.strictEqual(headers[':status'], 200);
req.on('response', common.mustCall((_headers, _flags, rawHeaders) => {
assert.deepStrictEqual(rawHeaders, [
':status', '200',
'x', '1',
'x-foo', 'bar', // Lowercased as required for HTTP/2
'x', '2', // Duplicate header order preserved
'date', '0000', // Server doesn't automatically set its own value
]);
client.close();
server.close();
}));
Expand Down
Loading