Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions benchmark/http/is_valid_header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use strict';

// Compares the non-throwing header validators (http.isValidHeaderName() and
// http.isValidHeaderValue()) with the throwing ones (http.validateHeaderName()
// and http.validateHeaderValue()) used inside try/catch.

const common = require('../common.js');
const assert = require('assert');
const {
isValidHeaderName,
isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
} = require('http');

const inputs = {
name: {
valid: [
'ETag', 'date', 'Vary', 'server', 'Expires', 'location', 'Connection',
'content-type', 'Cache-Control', 'content-length', 'x-frame-options',
'Transfer-Encoding', 'x-request-id',
],
invalid: [
'', ':', 'bad header', 'x-forwarded-fםr', '中文呢', '((((())))',
':alternate-protocol', 'alternate-protocol:', 'x\r\ninjected',
],
},
value: {
valid: [
'W/"2-d4cbb29"', 'OK', 'Express', 'application/json',
'application/json; charset=utf-8', 'sessionid=; Path=/',
'text/html; charset=utf-8', '10', 'max-age=0, no-cache', 'gzip, br',
],
// Invalid under both 'strict' and 'relaxed' validation.
invalid: [
'a\r\nb', 'value\n', 'cr\r', 'nul\0byte', 'לא תקין', 'emoji \u{1F600}',
'x'.repeat(64) + '\r\n',
],
},
};

const bench = common.createBenchmark(main, {
method: [
'isValidHeaderName',
'validateHeaderName',
'isValidHeaderValue',
'validateHeaderValue',
],
input: ['valid', 'invalid'],
httpValidation: ['strict', 'relaxed'],
n: [1e6],
}, {
// httpValidation only applies to isValidHeaderValue().
combinationFilter: (p) =>
p.httpValidation === 'strict' || p.method === 'isValidHeaderValue',
});

function main({ n, method, input, httpValidation }) {
let valid = 0;

switch (method) {
case 'isValidHeaderName': {
const list = inputs.name[input];
const len = list.length;
bench.start();
for (let i = 0; i < n; i++) {
if (isValidHeaderName(list[i % len])) valid++;
}
bench.end(n);
break;
}
case 'validateHeaderName': {
const list = inputs.name[input];
const len = list.length;
bench.start();
for (let i = 0; i < n; i++) {
try {
validateHeaderName(list[i % len]);
valid++;
} catch {
// Invalid name.
}
}
bench.end(n);
break;
}
case 'isValidHeaderValue': {
const list = inputs.value[input];
const len = list.length;
const options = httpValidation === 'strict' ? undefined : { httpValidation };
bench.start();
for (let i = 0; i < n; i++) {
if (isValidHeaderValue(list[i % len], options)) valid++;
}
bench.end(n);
break;
}
case 'validateHeaderValue': {
const list = inputs.value[input];
const len = list.length;
bench.start();
for (let i = 0; i < n; i++) {
try {
validateHeaderValue('x-header', list[i % len]);
valid++;
} catch {
// Invalid value.
}
}
bench.end(n);
break;
}
default:
throw new Error(`Unexpected method: ${method}`);
}

// Consume the result so the loop cannot be optimized away, and make sure
// the inputs are what they claim to be.
assert.strictEqual(valid, input === 'valid' ? n : 0);
}
86 changes: 86 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
44 changes: 42 additions & 2 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ const {
},
hideStackFrames,
} = require('internal/errors');
const { validateString } = require('internal/validators');
const {
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');
const {
assignFunctionName,
deprecateInstantiation,
Expand Down Expand Up @@ -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);
Comment thread
jasnell marked this conversation as resolved.
}

const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value, lenient) => {
if (value === undefined) {
throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
Expand Down Expand Up @@ -1424,6 +1462,8 @@ module.exports = {
kHighWaterMark,
kUniqueHeaders,
parseUniqueHeadersOption,
isValidHeaderName,
isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
OutgoingMessage,
Expand Down
4 changes: 4 additions & 0 deletions lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -192,6 +194,8 @@ module.exports = {
Server,
ServerResponse,
createServer,
isValidHeaderName,
isValidHeaderValue,
validateHeaderName,
validateHeaderValue,
get,
Expand Down
Loading
Loading