diff --git a/doc/api/http2.md b/doc/api/http2.md
index dc741338781bce..ae900aa31b47d5 100644
--- a/doc/api/http2.md
+++ b/doc/api/http2.md
@@ -2899,6 +2899,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] is emitted. If the
socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
+ * `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
+ and trailing whitespace validation for HTTP/2 header field names and values
+ as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
+ **Default:** `true`.
* ...: Any [`net.createServer()`][] option can be provided.
* `onRequestHandler` {Function} See [Compatibility API][]
* Returns: {Http2Server}
@@ -3070,6 +3074,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
the socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
+ * `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
+ and trailing whitespace validation for HTTP/2 header field names and values
+ as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
+ **Default:** `true`.
* `onRequestHandler` {Function} See [Compatibility API][]
* Returns: {Http2SecureServer}
@@ -3225,6 +3233,10 @@ changes:
a server should wait when an [`'unknownProtocol'`][] event is emitted. If
the socket has not been destroyed by that time the server will destroy it.
**Default:** `10000`.
+ * `strictFieldWhitespaceValidation` {boolean} If `true`, it turns on strict leading
+ and trailing whitespace validation for HTTP/2 header field names and values
+ as per [RFC-9113](https://www.rfc-editor.org/rfc/rfc9113.html#section-8.2.1).
+ **Default:** `true`.
* `listener` {Function} Will be registered as a one-time listener of the
[`'connect'`][] event.
* Returns: {ClientHttp2Session}
diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js
index 75312e5aa57c5f..396623d3b9d06f 100644
--- a/lib/internal/http2/util.js
+++ b/lib/internal/http2/util.js
@@ -229,7 +229,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
-const IDX_OPTIONS_FLAGS = 12;
+const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
+const IDX_OPTIONS_FLAGS = 13;
function updateOptionsBuffer(options) {
let flags = 0;
@@ -293,6 +294,13 @@ function updateOptionsBuffer(options) {
optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST] =
MathMax(1, options.streamResetBurst);
}
+
+ if (typeof options.strictFieldWhitespaceValidation === 'boolean') {
+ flags |= (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION);
+ optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
+ options.strictFieldWhitespaceValidation === true ? 0 : 1;
+ }
+
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}
diff --git a/src/node_http2.cc b/src/node_http2.cc
index 4415ea096d0ea0..17da8480756fca 100644
--- a/src/node_http2.cc
+++ b/src/node_http2.cc
@@ -159,6 +159,12 @@ Http2Options::Http2Options(Http2State* http2_state, SessionType type) {
buffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS]);
}
+ // Validate headers in accordance to RFC-9113
+ if (flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION)) {
+ nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation(
+ option, buffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION]);
+ }
+
// The padding strategy sets the mechanism by which we determine how much
// additional frame padding to apply to DATA and HEADERS frames. Currently
// this is set on a per-session basis, but eventually we may switch to
diff --git a/src/node_http2_state.h b/src/node_http2_state.h
index 2957a2827f370e..914ad011e021f1 100644
--- a/src/node_http2_state.h
+++ b/src/node_http2_state.h
@@ -60,6 +60,7 @@ namespace http2 {
IDX_OPTIONS_MAX_SETTINGS,
IDX_OPTIONS_STREAM_RESET_RATE,
IDX_OPTIONS_STREAM_RESET_BURST,
+ IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION,
IDX_OPTIONS_FLAGS
};
diff --git a/test/parallel/test-http2-server-rfc-9113-client.js b/test/parallel/test-http2-server-rfc-9113-client.js
new file mode 100644
index 00000000000000..caeb7e1fd74361
--- /dev/null
+++ b/test/parallel/test-http2-server-rfc-9113-client.js
@@ -0,0 +1,80 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const body =
+ '
this is some data';
+
+const server = http2.createServer((req, res) => {
+ res.setHeader('foobar', 'baz ');
+ res.setHeader('X-POWERED-BY', 'node-test\t');
+ res.setHeader('x-h2-header', '\tconnection-test');
+ res.setHeader('x-h2-header-2', ' connection-test');
+ res.setHeader('x-h2-header-3', 'connection-test ');
+ res.end(body);
+});
+
+const server2 = http2.createServer((req, res) => {
+ res.setHeader('foobar', 'baz ');
+ res.setHeader('X-POWERED-BY', 'node-test\t');
+ res.setHeader('x-h2-header', '\tconnection-test');
+ res.setHeader('x-h2-header-2', ' connection-test');
+ res.setHeader('x-h2-header-3', 'connection-test ');
+ res.end(body);
+});
+
+server.listen(0, common.mustCall(() => {
+ server2.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const client2 = http2.connect(`http://localhost:${server2.address().port}`, { strictFieldWhitespaceValidation: false });
+ const headers = { ':path': '/' };
+ const req = client.request(headers);
+
+ req.setEncoding('utf8');
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers.foobar, undefined);
+ assert.strictEqual(headers['x-powered-by'], undefined);
+ assert.strictEqual(headers['x-powered-by'], undefined);
+ assert.strictEqual(headers['x-h2-header'], undefined);
+ assert.strictEqual(headers['x-h2-header-2'], undefined);
+ assert.strictEqual(headers['x-h2-header-3'], undefined);
+ }));
+
+ let data = '';
+ req.on('data', (d) => data += d);
+ req.on('end', () => {
+ assert.strictEqual(body, data);
+ client.close();
+ client.on('close', common.mustCall(() => {
+ server.close();
+ }));
+
+ const req2 = client2.request(headers);
+ let data2 = '';
+ req2.setEncoding('utf8');
+ req2.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers.foobar, 'baz ');
+ assert.strictEqual(headers['x-powered-by'], 'node-test\t');
+ assert.strictEqual(headers['x-h2-header'], '\tconnection-test');
+ assert.strictEqual(headers['x-h2-header-2'], ' connection-test');
+ assert.strictEqual(headers['x-h2-header-3'], 'connection-test ');
+ }));
+ req2.on('data', (d) => data2 += d);
+ req2.on('end', () => {
+ assert.strictEqual(body, data2);
+ client2.close();
+ client2.on('close', common.mustCall(() => {
+ server2.close();
+ }));
+ });
+ req2.end();
+ });
+
+ req.end();
+ }));
+}));
+
+server.on('error', common.mustNotCall());
diff --git a/test/parallel/test-http2-server-rfc-9113-server.js b/test/parallel/test-http2-server-rfc-9113-server.js
new file mode 100644
index 00000000000000..b05bdb2f8cbc37
--- /dev/null
+++ b/test/parallel/test-http2-server-rfc-9113-server.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+const assert = require('assert');
+const http2 = require('http2');
+const body =
+ 'this is some data';
+
+const server = http2.createServer((req, res) => {
+ assert.strictEqual(req.headers['x-powered-by'], undefined);
+ assert.strictEqual(req.headers.foobar, undefined);
+ assert.strictEqual(req.headers['x-h2-header'], undefined);
+ assert.strictEqual(req.headers['x-h2-header-2'], undefined);
+ assert.strictEqual(req.headers['x-h2-header-3'], undefined);
+ assert.strictEqual(req.headers['x-h2-header-4'], undefined);
+ res.writeHead(200);
+ res.end(body);
+});
+
+const server2 = http2.createServer({ strictFieldWhitespaceValidation: false }, (req, res) => {
+ assert.strictEqual(req.headers.foobar, 'baz ');
+ assert.strictEqual(req.headers['x-powered-by'], 'node-test\t');
+ assert.strictEqual(req.headers['x-h2-header'], '\tconnection-test');
+ assert.strictEqual(req.headers['x-h2-header-2'], ' connection-test');
+ assert.strictEqual(req.headers['x-h2-header-3'], 'connection-test ');
+ assert.strictEqual(req.headers['x-h2-header-4'], 'connection-test\t');
+ res.writeHead(200);
+ res.end(body);
+});
+
+server.listen(0, common.mustCall(() => {
+ server2.listen(0, common.mustCall(() => {
+ const client = http2.connect(`http://localhost:${server.address().port}`);
+ const client2 = http2.connect(`http://localhost:${server2.address().port}`);
+ const headers = {
+ 'foobar': 'baz ',
+ ':path': '/',
+ 'x-powered-by': 'node-test\t',
+ 'x-h2-header': '\tconnection-test',
+ 'x-h2-header-2': ' connection-test',
+ 'x-h2-header-3': 'connection-test ',
+ 'x-h2-header-4': 'connection-test\t'
+ };
+ const req = client.request(headers);
+
+ req.setEncoding('utf8');
+ req.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers[':status'], 200);
+ }));
+
+ let data = '';
+ req.on('data', (d) => data += d);
+ req.on('end', () => {
+ assert.strictEqual(body, data);
+ client.close();
+ client.on('close', common.mustCall(() => {
+ server.close();
+ }));
+
+ const req2 = client2.request(headers);
+ let data2 = '';
+ req2.setEncoding('utf8');
+ req2.on('response', common.mustCall(function(headers) {
+ assert.strictEqual(headers[':status'], 200);
+ }));
+ req2.on('data', (d) => data2 += d);
+ req2.on('end', () => {
+ assert.strictEqual(body, data2);
+ client2.close();
+ client2.on('close', common.mustCall(() => {
+ server2.close();
+ }));
+ });
+ req2.end();
+ });
+
+ req.end();
+ }));
+}));
+
+server.on('error', common.mustNotCall());
diff --git a/test/parallel/test-http2-util-update-options-buffer.js b/test/parallel/test-http2-util-update-options-buffer.js
index c370fe50c07439..26e220e6b7b507 100644
--- a/test/parallel/test-http2-util-update-options-buffer.js
+++ b/test/parallel/test-http2-util-update-options-buffer.js
@@ -25,7 +25,8 @@ const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
-const IDX_OPTIONS_FLAGS = 12;
+const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
+const IDX_OPTIONS_FLAGS = 13;
{
updateOptionsBuffer({
@@ -41,6 +42,7 @@ const IDX_OPTIONS_FLAGS = 12;
maxSettings: 10,
streamResetRate: 11,
streamResetBurst: 12,
+ strictFieldWhitespaceValidation: false
});
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -55,6 +57,7 @@ const IDX_OPTIONS_FLAGS = 12;
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SETTINGS], 10);
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE], 11);
strictEqual(optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST], 12);
+ strictEqual(optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION], 1);
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
@@ -69,6 +72,7 @@ const IDX_OPTIONS_FLAGS = 12;
ok(flags & (1 << IDX_OPTIONS_MAX_SETTINGS));
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_RATE));
ok(flags & (1 << IDX_OPTIONS_STREAM_RESET_BURST));
+ ok(flags & (1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION));
}
{