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>
This commit is contained in:
James M Snell committed 2026-09-29 04:38:26 +00:00
1 parent 3a30bcad56
commit 44ba289f55
4 files changed
+300 -2

No files matched your search

+86
View File
@@ -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
+42 -2
View File
@@ -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,
+4
View File
@@ -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,
+168
View File
@@ -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',
});
}
}