Skip to content

Commit e998d26

Browse files
jasnelladuh95
authored andcommitted
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: #66334 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
1 parent f9da049 commit e998d26

4 files changed

Lines changed: 300 additions & 2 deletions

File tree

‎doc/api/http.md‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4375,6 +4375,89 @@ request. Specifically, the `'error'` event will be emitted with an error with
43754375
the message `'AbortError: The operation was aborted'`, the code `'ABORT_ERR'`
43764376
and the `cause`, if one was provided.
43774377
4378+
## `http.isValidHeaderName(name)`
4379+
4380+
<!-- YAML
4381+
added: REPLACEME
4382+
-->
4383+
4384+
* `name` {any}
4385+
* Returns: {boolean}
4386+
4387+
Returns `true` if `name` is a valid HTTP header name (a non-empty string that
4388+
is an HTTP [token][]), and `false` otherwise. This is the same check that
4389+
[`http.validateHeaderName()`][] performs, but the result is returned instead of
4390+
an error being thrown, so it is suitable for use in hot paths where invalid
4391+
input is expected.
4392+
4393+
HTTP methods are also tokens, so this function can validate them as well.
4394+
4395+
```mjs
4396+
import { isValidHeaderName } from 'node:http';
4397+
4398+
console.log(isValidHeaderName('content-type')); // true
4399+
console.log(isValidHeaderName('X-Request-Id')); // true
4400+
console.log(isValidHeaderName('')); // false
4401+
console.log(isValidHeaderName('bad header')); // false
4402+
console.log(isValidHeaderName(42)); // false
4403+
```
4404+
4405+
```cjs
4406+
const { isValidHeaderName } = require('node:http');
4407+
4408+
console.log(isValidHeaderName('content-type')); // true
4409+
console.log(isValidHeaderName('X-Request-Id')); // true
4410+
console.log(isValidHeaderName('')); // false
4411+
console.log(isValidHeaderName('bad header')); // false
4412+
console.log(isValidHeaderName(42)); // false
4413+
```
4414+
4415+
## `http.isValidHeaderValue(value[, options])`
4416+
4417+
<!-- YAML
4418+
added: REPLACEME
4419+
-->
4420+
4421+
* `value` {any}
4422+
* `options` {Object}
4423+
* `httpValidation` {string} Validation strictness, one of `'strict'` or
4424+
`'relaxed'`. These have the same meaning as the `httpValidation` option of
4425+
[`http.createServer()`][] and [`http.request()`][]. **Default:** `'strict'`.
4426+
* Returns: {boolean}
4427+
4428+
Returns `true` if `value` is a valid HTTP header value, and `false` otherwise.
4429+
With the default options this is the same check that
4430+
[`http.validateHeaderValue()`][] performs, but the result is returned instead
4431+
of an error being thrown.
4432+
4433+
`undefined` and symbols are never valid header values. Other non-string
4434+
values are converted to strings before being checked, as they are when passed
4435+
to [`outgoingMessage.setHeader(name, value)`][].
4436+
4437+
Passing an invalid `options` argument throws.
4438+
4439+
```mjs
4440+
import { isValidHeaderValue } from 'node:http';
4441+
4442+
console.log(isValidHeaderValue('text/html')); // true
4443+
console.log(isValidHeaderValue(123)); // true
4444+
console.log(isValidHeaderValue(undefined)); // false
4445+
console.log(isValidHeaderValue('a\r\nb')); // false
4446+
console.log(isValidHeaderValue('a\x01b')); // false
4447+
console.log(isValidHeaderValue('a\x01b', { httpValidation: 'relaxed' })); // true
4448+
```
4449+
4450+
```cjs
4451+
const { isValidHeaderValue } = require('node:http');
4452+
4453+
console.log(isValidHeaderValue('text/html')); // true
4454+
console.log(isValidHeaderValue(123)); // true
4455+
console.log(isValidHeaderValue(undefined)); // false
4456+
console.log(isValidHeaderValue('a\r\nb')); // false
4457+
console.log(isValidHeaderValue('a\x01b')); // false
4458+
console.log(isValidHeaderValue('a\x01b', { httpValidation: 'relaxed' })); // true
4459+
```
4460+
43784461
## `http.validateHeaderName(name[, label])`
43794462
43804463
<!-- YAML
@@ -4767,6 +4850,8 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
47674850
[`http.globalAgent`]: #httpglobalagent
47684851
[`http.request()`]: #httprequestoptions-callback
47694852
[`http.setGlobalProxyFromEnv()`]: #httpsetglobalproxyfromenvproxyenv
4853+
[`http.validateHeaderName()`]: #httpvalidateheadernamename-label
4854+
[`http.validateHeaderValue()`]: #httpvalidateheadervaluename-value
47704855
[`message.headers`]: #messageheaders
47714856
[`message.rawHeaders`]: #messagerawheaders
47724857
[`message.socket`]: #messagesocket
@@ -4829,3 +4914,4 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
48294914
[information event]: #event-information
48304915
[initial delay]: net.md#socketsetkeepaliveenable-initialdelay-interval-count
48314916
[request target]: https://datatracker.ietf.org/doc/html/rfc9112#section-3.2
4917+
[token]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2

‎lib/_http_outgoing.js‎

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ const {
7171
},
7272
hideStackFrames,
7373
} = require('internal/errors');
74-
const { validateString } = require('internal/validators');
74+
const {
75+
validateObject,
76+
validateOneOf,
77+
validateString,
78+
} = require('internal/validators');
7579
const { assignFunctionName } = require('internal/util');
7680
const { isUint8Array } = require('internal/util/types');
7781

@@ -716,11 +720,45 @@ function matchHeader(self, state, field, value) {
716720
}
717721

718722
const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFrames((name, label) => {
719-
if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) {
723+
if (!isValidHeaderName(name)) {
720724
throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name);
721725
}
722726
}));
723727

728+
/**
729+
* Non-throwing counterpart of `validateHeaderName()`.
730+
* @param {any} name
731+
* @returns {boolean}
732+
*/
733+
function isValidHeaderName(name) {
734+
return typeof name === 'string' && checkIsHttpToken(name);
735+
}
736+
737+
const kHttpValidationValues = ['strict', 'relaxed'];
738+
739+
/**
740+
* Non-throwing counterpart of `validateHeaderValue()`.
741+
* @param {any} value
742+
* @param {{ httpValidation?: 'strict' | 'relaxed' }} [options]
743+
* @returns {boolean}
744+
*/
745+
function isValidHeaderValue(value, options) {
746+
let lenient = false;
747+
if (options !== undefined) {
748+
validateObject(options, 'options');
749+
const { httpValidation } = options;
750+
if (httpValidation === 'relaxed') {
751+
lenient = true;
752+
} else if (httpValidation !== undefined && httpValidation !== 'strict') {
753+
validateOneOf(httpValidation, 'options.httpValidation', kHttpValidationValues);
754+
}
755+
}
756+
if (value === undefined || typeof value === 'symbol') {
757+
return false;
758+
}
759+
return !checkInvalidHeaderChar(value, lenient);
760+
}
761+
724762
const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => {
725763
if (value === undefined) {
726764
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
@@ -1413,6 +1451,8 @@ module.exports = {
14131451
kHighWaterMark,
14141452
kUniqueHeaders,
14151453
parseUniqueHeadersOption,
1454+
isValidHeaderName,
1455+
isValidHeaderValue,
14161456
validateHeaderName,
14171457
validateHeaderValue,
14181458
OutgoingMessage,

‎lib/http.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const { methods, parsers } = require('_http_common');
3232
const { IncomingMessage } = require('_http_incoming');
3333
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
3434
const {
35+
isValidHeaderName,
36+
isValidHeaderValue,
3537
validateHeaderName,
3638
validateHeaderValue,
3739
OutgoingMessage,
@@ -192,6 +194,8 @@ module.exports = {
192194
Server,
193195
ServerResponse,
194196
createServer,
197+
isValidHeaderName,
198+
isValidHeaderValue,
195199
validateHeaderName,
196200
validateHeaderValue,
197201
get,
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const {
5+
isValidHeaderName,
6+
isValidHeaderValue,
7+
validateHeaderName,
8+
validateHeaderValue,
9+
} = require('http');
10+
11+
function succeeds(fn) {
12+
try {
13+
fn();
14+
return true;
15+
} catch {
16+
return false;
17+
}
18+
}
19+
20+
// isValidHeaderName
21+
{
22+
const valid = [
23+
'a',
24+
'user-agent',
25+
'USER-AGENT',
26+
'User-Agent',
27+
'x-forwarded-for',
28+
'x-request-id-with-a-long-name',
29+
"!#$%&'*+-.^_`|~",
30+
'0123456789',
31+
];
32+
const invalid = [
33+
'',
34+
' ',
35+
'bad header',
36+
'bad:header',
37+
'x-forwarded-fםr',
38+
'איקס-פורוורד-פור',
39+
'x\r\ny',
40+
'x\0',
41+
'(comment)',
42+
'"quoted"',
43+
'a,b',
44+
'long-invalid-header-name\u00e9',
45+
];
46+
const nonStrings = [
47+
undefined, null, 0, 1, true, false, {}, [], ['a'],
48+
Symbol('a'), () => {}, 1n, Buffer.from('a'),
49+
];
50+
51+
for (const name of valid) {
52+
assert.strictEqual(isValidHeaderName(name), true, name);
53+
}
54+
for (const name of [...invalid, ...nonStrings]) {
55+
assert.strictEqual(isValidHeaderName(name), false, String(name?.toString?.()));
56+
}
57+
58+
// Must agree with validateHeaderName() for every input.
59+
for (const name of [...valid, ...invalid, ...nonStrings]) {
60+
assert.strictEqual(
61+
isValidHeaderName(name),
62+
succeeds(() => validateHeaderName(name)),
63+
);
64+
}
65+
66+
// Every single-character name agrees with validateHeaderName(), for both
67+
// the short (lookup table) and long (regexp) code paths.
68+
for (let c = 0; c <= 0x10ff; c++) {
69+
const ch = String.fromCharCode(c);
70+
for (const name of [ch, `${ch}xxxxxxxxxxxx`]) {
71+
assert.strictEqual(
72+
isValidHeaderName(name),
73+
succeeds(() => validateHeaderName(name)),
74+
`char code ${c}`,
75+
);
76+
}
77+
}
78+
}
79+
80+
// isValidHeaderValue
81+
{
82+
const valid = [
83+
'',
84+
'text/html',
85+
'a b\tc',
86+
'\u00e9\u00ff',
87+
'\x80',
88+
1,
89+
0,
90+
null,
91+
true,
92+
['a', 'b'],
93+
];
94+
const invalid = [
95+
undefined,
96+
'a\r\nb',
97+
'a\nb',
98+
'a\rb',
99+
'a\0b',
100+
'a\x01b',
101+
'a\x7fb',
102+
'לא תקין',
103+
'\u0100',
104+
['a', 'b\n'],
105+
Symbol('a'),
106+
];
107+
108+
for (const value of valid) {
109+
assert.strictEqual(isValidHeaderValue(value), true, String(value));
110+
}
111+
for (const value of invalid) {
112+
assert.strictEqual(isValidHeaderValue(value), false, String(value));
113+
}
114+
115+
// Must agree with validateHeaderValue() for every input.
116+
for (const value of [...valid, ...invalid]) {
117+
assert.strictEqual(
118+
isValidHeaderValue(value),
119+
succeeds(() => validateHeaderValue('x-test', value)),
120+
);
121+
}
122+
123+
for (let c = 0; c <= 0x10ff; c++) {
124+
const value = `a${String.fromCharCode(c)}b`;
125+
assert.strictEqual(
126+
isValidHeaderValue(value),
127+
succeeds(() => validateHeaderValue('x-test', value)),
128+
`char code ${c}`,
129+
);
130+
// Explicit 'strict' is the same as the default.
131+
assert.strictEqual(
132+
isValidHeaderValue(value, { httpValidation: 'strict' }),
133+
isValidHeaderValue(value),
134+
`char code ${c}`,
135+
);
136+
}
137+
138+
// 'relaxed' follows the Fetch spec: only NUL, CR, LF and code points above
139+
// U+00FF are rejected.
140+
const relaxed = { httpValidation: 'relaxed' };
141+
for (let c = 0; c <= 0x10ff; c++) {
142+
const expected = !(c === 0x00 || c === 0x0a || c === 0x0d || c > 0xff);
143+
assert.strictEqual(
144+
isValidHeaderValue(`a${String.fromCharCode(c)}b`, relaxed),
145+
expected,
146+
`char code ${c}`,
147+
);
148+
}
149+
assert.strictEqual(isValidHeaderValue(undefined, relaxed), false);
150+
assert.strictEqual(isValidHeaderValue('a\x01b', relaxed), true);
151+
assert.strictEqual(isValidHeaderValue('a\x7fb', relaxed), true);
152+
153+
// An empty options object uses the default.
154+
assert.strictEqual(isValidHeaderValue('a\x01b', {}), false);
155+
assert.strictEqual(isValidHeaderValue('a\x01b', { httpValidation: undefined }), false);
156+
157+
// Invalid options throw.
158+
for (const options of [null, 1, 'relaxed', true]) {
159+
assert.throws(() => isValidHeaderValue('a', options), {
160+
code: 'ERR_INVALID_ARG_TYPE',
161+
});
162+
}
163+
for (const httpValidation of ['insecure', 'RELAXED', '', 1, null]) {
164+
assert.throws(() => isValidHeaderValue('a', { httpValidation }), {
165+
code: 'ERR_INVALID_ARG_VALUE',
166+
});
167+
}
168+
}

0 commit comments

Comments
 (0)