mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
3a30bcad56
commit
44ba289f55
4 files changed
+300
-2
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user