Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
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}
* `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');
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
// 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
66 changes: 66 additions & 0 deletions test/parallel/test-http2-raw-headers-defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const http2 = require('http2');

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


server.listen(0, common.mustCall(() => {
const port = server.address().port;
const client = http2.connect(`http://localhost:${port}`);

const req = client.request([
'a', 'b',
'x-FOO', 'bar',
'a', 'c',
]).end();

assert.deepStrictEqual(req.sentHeaders, {
'__proto__': null,
':path': '/',
':scheme': 'http',
':authority': `localhost:${server.address().port}`,
':method': 'GET',
'a': [ 'b', 'c' ],
'x-FOO': 'bar',
});

req.on('response', common.mustCall((_headers, _flags, rawHeaders) => {
assert.strictEqual(rawHeaders.length, 10);
assert.deepStrictEqual(rawHeaders.slice(0, 8), [
':status', '200',
'x', '1',
'x-foo', 'bar', // Lowercased as required for HTTP/2
'x', '2', // Duplicate header order preserved
]);

assert.strictEqual(rawHeaders[8], 'date');
assert.strictEqual(typeof rawHeaders[9], 'string');

client.close();
server.close();
}));
}));
}
46 changes: 33 additions & 13 deletions test/parallel/test-http2-raw-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,33 @@ 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',
':authority', `test.invalid:${server.address().port}`,
':method', 'POST',
'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', '404',
'x', '1',
'x-FOO', 'bar',
'x', '2',
'DATE', '0000',
]);
stream.respond({
':status': 200

assert.deepStrictEqual(stream.sentHeaders, {
'__proto__': null,
':status': '404',
'x': [ '1', '2' ],
'x-FOO': 'bar',
'DATE': '0000',
});

stream.end();
}));

Expand All @@ -32,8 +46,8 @@ const http2 = require('http2');
const req = client.request([
':path', '/foobar',
':scheme', 'http',
':authority', `localhost:${server.address().port}`,
':method', 'GET',
':authority', `test.invalid:${server.address().port}`,
':method', 'POST',
'a', 'b',
'x-FOO', 'bar',
'a', 'c',
Expand All @@ -43,14 +57,20 @@ const http2 = require('http2');
'__proto__': null,
':path': '/foobar',
':scheme': 'http',
':authority': `localhost:${server.address().port}`,
':method': 'GET',
':authority': `test.invalid:${server.address().port}`,
':method': 'POST',
'a': [ 'b', 'c' ],
'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', '404',
'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