diff --git a/benchmark/http/is_valid_header.js b/benchmark/http/is_valid_header.js new file mode 100644 index 000000000000..cefe5c1f87ce --- /dev/null +++ b/benchmark/http/is_valid_header.js @@ -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); +} diff --git a/doc/api/http.md b/doc/api/http.md index c4888f2bf660..5351b05eeaa9 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)` + + + +* `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])` + + + +* `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])`