Commit 44ba289f55f for nodejs
commit 44ba289f55f3dbfa932ab159f73ba29c2e645586
Author: James M Snell <jasnell@gmail.com>
Date: Sun Sep 27 02:18:02 2026 +0000
http: add isValidHeaderName() and isValidHeaderValue()
Add non-throwing counterparts of http.validateHeaderName() and
http.validateHeaderValue() that return a boolean instead of throwing.
Rejecting an invalid header with the existing validators costs a few
microseconds, because an error object and its stack trace are created,
compared to ~20ns for the boolean check. Userland HTTP implementations
such as undici (fetch Headers, request options) therefore keep private
copies of the token and field-value tables from _http_common. These new
functions let them reuse the core implementation.
isValidHeaderValue() accepts an optional `httpValidation` option
('strict' or 'relaxed') that has the same meaning as the option of the
same name on http.createServer() and http.request().
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66334
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
diff --git a/doc/api/http.md b/doc/api/http.md
index c4888f2bf66..5351b05eeaa 100644
--- a/doc/api/http.md
+++ b/doc/api/http.md
@@ -4403,6 +4403,89 @@ request. Specifically, the `'error'` event will be emitted with an error with
the message `'AbortError: The operation was aborted'`, the code `'ABORT_ERR'`
and the `cause`, if one was provided.
+## `http.isValidHeaderName(name)`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `name` {any}
+* Returns: {boolean}
+
+Returns `true` if `name` is a valid HTTP header name (a non-empty string that
+is an HTTP [token][]), and `false` otherwise. This is the same check that
+[`http.validateHeaderName()`][] performs, but the result is returned instead of
+an error being thrown, so it is suitable for use in hot paths where invalid
+input is expected.
+
+HTTP methods are also tokens, so this function can validate them as well.
+
+```mjs
+import { isValidHeaderName } from 'node:http';
+
+console.log(isValidHeaderName('content-type')); // true
+console.log(isValidHeaderName('X-Request-Id')); // true
+console.log(isValidHeaderName('')); // false
+console.log(isValidHeaderName('bad header')); // false
+console.log(isValidHeaderName(42)); // false
+```
+
+```cjs
+const { isValidHeaderName } = require('node:http');
+
+console.log(isValidHeaderName('content-type')); // true
+console.log(isValidHeaderName('X-Request-Id')); // true
+console.log(isValidHeaderName('')); // false
+console.log(isValidHeaderName('bad header')); // false
+console.log(isValidHeaderName(42)); // false
+```
+
+## `http.isValidHeaderValue(value[, options])`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+* `value` {any}
+* `options` {Object}
+ * `httpValidation` {string} Validation strictness, one of `'strict'` or
+ `'relaxed'`. These have the same meaning as the `httpValidation` option of
+ [`http.createServer()`][] and [`http.request()`][]. **Default:** `'strict'`.
+* Returns: {boolean}
+
+Returns `true` if `value` is a valid HTTP header value, and `false` otherwise.
+With the default options this is the same check that
+[`http.validateHeaderValue()`][] performs, but the result is returned instead
+of an error being thrown.
+
+`undefined` and symbols are never valid header values. Other non-string
+values are converted to strings before being checked, as they are when passed
+to [`outgoingMessage.setHeader(name, value)`][].
+
+Passing an invalid `options` argument throws.
+
+```mjs
+import { isValidHeaderValue } from 'node:http';
+
+console.log(isValidHeaderValue('text/html')); // true
+console.log(isValidHeaderValue(123)); // true
+console.log(isValidHeaderValue(undefined)); // false
+console.log(isValidHeaderValue('a\r\nb')); // false
+console.log(isValidHeaderValue('a\x01b')); // false
+console.log(isValidHeaderValue('a\x01b', { httpValidation: 'relaxed' })); // true
+```
+
+```cjs
+const { isValidHeaderValue } = require('node:http');
+
+console.log(isValidHeaderValue('text/html')); // true
+console.log(isValidHeaderValue(123)); // true
+console.log(isValidHeaderValue(undefined)); // false
+console.log(isValidHeaderValue('a\r\nb')); // false
+console.log(isValidHeaderValue('a\x01b')); // false
+console.log(isValidHeaderValue('a\x01b', { httpValidation: 'relaxed' })); // true
+```
+
## `http.validateHeaderName(name[, label])`
<!-- YAML
@@ -4795,6 +4878,8 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
[`http.globalAgent`]: #httpglobalagent
[`http.request()`]: #httprequestoptions-callback
[`http.setGlobalProxyFromEnv()`]: #httpsetglobalproxyfromenvproxyenv
+[`http.validateHeaderName()`]: #httpvalidateheadernamename-label
+[`http.validateHeaderValue()`]: #httpvalidateheadervaluename-value
[`message.headers`]: #messageheaders
[`message.rawHeaders`]: #messagerawheaders
[`message.socket`]: #messagesocket
@@ -4857,3 +4942,4 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
[information event]: #event-information
[initial delay]: net.md#socketsetkeepaliveenable-initialdelay-interval-count
[request target]: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
+[token]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index 0b728f6ae4d..f6713d7b06a 100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
@@ -71,7 +71,11 @@ const {
},
hideStackFrames,
} = require('internal/errors');
-const { validateString } = require('internal/validators');
+const {
+ validateObject,
+ validateOneOf,
+ validateString,
+} = require('internal/validators');
const {
assignFunctionName,
deprecateInstantiation,
@@ -723,11 +727,45 @@ function matchHeader(self, state, field, value) {
}
const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFrames((name, label) => {
- if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) {
+ if (!isValidHeaderName(name)) {
throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name);
}
}));
+/**
+ * Non-throwing counterpart of `validateHeaderName()`.
+ * @param {any} name
+ * @returns {boolean}
+ */
+function isValidHeaderName(name) {
+ return typeof name === 'string' && checkIsHttpToken(name);
+}
+
+const kHttpValidationValues = ['strict', 'relaxed'];
+
+/**
+ * Non-throwing counterpart of `validateHeaderValue()`.
+ * @param {any} value
+ * @param {{ httpValidation?: 'strict' | 'relaxed' }} [options]
+ * @returns {boolean}
+ */
+function isValidHeaderValue(value, options) {
+ let lenient = false;
+ if (options !== undefined) {
+ validateObject(options, 'options');
+ const { httpValidation } = options;
+ if (httpValidation === 'relaxed') {
+ lenient = true;
+ } else if (httpValidation !== undefined && httpValidation !== 'strict') {
+ validateOneOf(httpValidation, 'options.httpValidation', kHttpValidationValues);
+ }
+ }
+ if (value === undefined || typeof value === 'symbol') {
+ return false;
+ }
+ return !checkInvalidHeaderChar(value, lenient);
+}
+
const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => {
if (value === undefined) {
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
@@ -1424,6 +1462,8 @@ module.exports = {
kHighWaterMark,
kUniqueHeaders,
parseUniqueHeadersOption,
+ isValidHeaderName,
+ isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
OutgoingMessage,
diff --git a/lib/http.js b/lib/http.js
index 934d0b14bfd..783366e9fac 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -32,6 +32,8 @@ const { methods, parsers } = require('_http_common');
const { IncomingMessage } = require('_http_incoming');
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
const {
+ isValidHeaderName,
+ isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
OutgoingMessage,
@@ -192,6 +194,8 @@ module.exports = {
Server,
ServerResponse,
createServer,
+ isValidHeaderName,
+ isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
get,
diff --git a/test/parallel/test-http-is-valid-header.js b/test/parallel/test-http-is-valid-header.js
new file mode 100644
index 00000000000..ddaacdd4b2b
--- /dev/null
+++ b/test/parallel/test-http-is-valid-header.js
@@ -0,0 +1,168 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const {
+ isValidHeaderName,
+ isValidHeaderValue,
+ validateHeaderName,
+ validateHeaderValue,
+} = require('http');
+
+function succeeds(fn) {
+ try {
+ fn();
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+// isValidHeaderName
+{
+ const valid = [
+ 'a',
+ 'user-agent',
+ 'USER-AGENT',
+ 'User-Agent',
+ 'x-forwarded-for',
+ 'x-request-id-with-a-long-name',
+ "!#$%&'*+-.^_`|~",
+ '0123456789',
+ ];
+ const invalid = [
+ '',
+ ' ',
+ 'bad header',
+ 'bad:header',
+ 'x-forwarded-fםr',
+ 'איקס-פורוורד-פור',
+ 'x\r\ny',
+ 'x\0',
+ '(comment)',
+ '"quoted"',
+ 'a,b',
+ 'long-invalid-header-name\u00e9',
+ ];
+ const nonStrings = [
+ undefined, null, 0, 1, true, false, {}, [], ['a'],
+ Symbol('a'), () => {}, 1n, Buffer.from('a'),
+ ];
+
+ for (const name of valid) {
+ assert.strictEqual(isValidHeaderName(name), true, name);
+ }
+ for (const name of [...invalid, ...nonStrings]) {
+ assert.strictEqual(isValidHeaderName(name), false, String(name?.toString?.()));
+ }
+
+ // Must agree with validateHeaderName() for every input.
+ for (const name of [...valid, ...invalid, ...nonStrings]) {
+ assert.strictEqual(
+ isValidHeaderName(name),
+ succeeds(() => validateHeaderName(name)),
+ );
+ }
+
+ // Every single-character name agrees with validateHeaderName(), for both
+ // the short (lookup table) and long (regexp) code paths.
+ for (let c = 0; c <= 0x10ff; c++) {
+ const ch = String.fromCharCode(c);
+ for (const name of [ch, `${ch}xxxxxxxxxxxx`]) {
+ assert.strictEqual(
+ isValidHeaderName(name),
+ succeeds(() => validateHeaderName(name)),
+ `char code ${c}`,
+ );
+ }
+ }
+}
+
+// isValidHeaderValue
+{
+ const valid = [
+ '',
+ 'text/html',
+ 'a b\tc',
+ '\u00e9\u00ff',
+ '\x80',
+ 1,
+ 0,
+ null,
+ true,
+ ['a', 'b'],
+ ];
+ const invalid = [
+ undefined,
+ 'a\r\nb',
+ 'a\nb',
+ 'a\rb',
+ 'a\0b',
+ 'a\x01b',
+ 'a\x7fb',
+ 'לא תקין',
+ '\u0100',
+ ['a', 'b\n'],
+ Symbol('a'),
+ ];
+
+ for (const value of valid) {
+ assert.strictEqual(isValidHeaderValue(value), true, String(value));
+ }
+ for (const value of invalid) {
+ assert.strictEqual(isValidHeaderValue(value), false, String(value));
+ }
+
+ // Must agree with validateHeaderValue() for every input.
+ for (const value of [...valid, ...invalid]) {
+ assert.strictEqual(
+ isValidHeaderValue(value),
+ succeeds(() => validateHeaderValue('x-test', value)),
+ );
+ }
+
+ for (let c = 0; c <= 0x10ff; c++) {
+ const value = `a${String.fromCharCode(c)}b`;
+ assert.strictEqual(
+ isValidHeaderValue(value),
+ succeeds(() => validateHeaderValue('x-test', value)),
+ `char code ${c}`,
+ );
+ // Explicit 'strict' is the same as the default.
+ assert.strictEqual(
+ isValidHeaderValue(value, { httpValidation: 'strict' }),
+ isValidHeaderValue(value),
+ `char code ${c}`,
+ );
+ }
+
+ // 'relaxed' follows the Fetch spec: only NUL, CR, LF and code points above
+ // U+00FF are rejected.
+ const relaxed = { httpValidation: 'relaxed' };
+ for (let c = 0; c <= 0x10ff; c++) {
+ const expected = !(c === 0x00 || c === 0x0a || c === 0x0d || c > 0xff);
+ assert.strictEqual(
+ isValidHeaderValue(`a${String.fromCharCode(c)}b`, relaxed),
+ expected,
+ `char code ${c}`,
+ );
+ }
+ assert.strictEqual(isValidHeaderValue(undefined, relaxed), false);
+ assert.strictEqual(isValidHeaderValue('a\x01b', relaxed), true);
+ assert.strictEqual(isValidHeaderValue('a\x7fb', relaxed), true);
+
+ // An empty options object uses the default.
+ assert.strictEqual(isValidHeaderValue('a\x01b', {}), false);
+ assert.strictEqual(isValidHeaderValue('a\x01b', { httpValidation: undefined }), false);
+
+ // Invalid options throw.
+ for (const options of [null, 1, 'relaxed', true]) {
+ assert.throws(() => isValidHeaderValue('a', options), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ });
+ }
+ for (const httpValidation of ['insecure', 'RELAXED', '', 1, null]) {
+ assert.throws(() => isValidHeaderValue('a', { httpValidation }), {
+ code: 'ERR_INVALID_ARG_VALUE',
+ });
+ }
+}