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',
+    });
+  }
+}