From f33e86649e20926fe7cac01cc1bd84e6444b44c9 Mon Sep 17 00:00:00 2001
From: osher <osher.filter@gmail.com>
Date: Tue, 28 Apr 2020 12:30:20 +0300
Subject: [PATCH] http: expose http.validate-header-name/value

The use-case is for any framework that provides user mw a response
replacement, that collects the desired response state, and applies them
only on conclusion. As such a framework, I'd want to validate the
header names and values as soon as the user-code provides them.
This - to eliminate errors on response-send time, and provide developer
stack trace that contains the line that submits the offending values.

PR-URL: https://github.com/nodejs/node/pull/33119
Reviewed-By: Anna Henningsen <anna@addaleax.net>
---
 doc/api/http.md                              | 70 ++++++++++++++++++++
 lib/_http_outgoing.js                        |  3 +
 lib/http.js                                  |  3 +
 test/parallel/test-http-header-validators.js | 62 +++++++++++++++++
 4 files changed, 138 insertions(+)
 create mode 100644 test/parallel/test-http-header-validators.js

diff --git a/doc/api/http.md b/doc/api/http.md
index 38bd42f119bff7..b315f0005f3b6a 100644
--- a/doc/api/http.md
+++ b/doc/api/http.md
@@ -2465,6 +2465,76 @@ events will be emitted in the following order:
 Setting the `timeout` option or using the `setTimeout()` function will
 not abort the request or do anything besides add a `'timeout'` event.
 
+## `http.validateHeaderName(name)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `name` {string}
+
+Performs the low-level validations on the provided `name` that are done when
+`res.setHeader(name, value)` is called.
+
+Passing illegal value as `name` will result in a [`TypeError`][] being thrown,
+identified by `code: 'ERR_INVALID_HTTP_TOKEN'`.
+
+It is not necessary to use this method before passing headers to an HTTP request
+or response. The HTTP module will automatically validate such headers.
+Examples:
+
+Example:
+```js
+const { validateHeaderName } = require('http');
+
+try {
+  validateHeaderName('');
+} catch (err) {
+  err instanceof TypeError; // --> true
+  err.code; // --> 'ERR_INVALID_HTTP_TOKEN'
+  err.message; // --> 'Header name must be a valid HTTP token [""]'
+}
+```
+
+## `http.validateHeaderValue(name, value)`
+<!-- YAML
+added: REPLACEME
+-->
+
+* `name` {string}
+* `value` {any}
+
+Performs the low-level validations on the provided `value` that are done when
+`res.setHeader(name, value)` is called.
+
+Passing illegal value as `value` will result in a [`TypeError`][] being thrown.
+* Undefined value error is identified by `code: 'ERR_HTTP_INVALID_HEADER_VALUE'`.
+* Invalid value character error is identified by `code: 'ERR_INVALID_CHAR'`.
+
+It is not necessary to use this method before passing headers to an HTTP request
+or response. The HTTP module will automatically validate such headers.
+
+Examples:
+
+```js
+const { validateHeaderValue } = require('http');
+
+try {
+  validateHeaderValue('x-my-header', undefined);
+} catch (err) {
+  err instanceof TypeError; // --> true
+  err.code === 'ERR_HTTP_INVALID_HEADER_VALUE'; // --> true
+  err.message; // --> 'Invalid value "undefined" for header "x-my-header"'
+}
+
+try {
+  validateHeaderValue('x-my-header', 'oʊmɪɡə');
+} catch (err) {
+  err instanceof TypeError; // --> true
+  err.code === 'ERR_INVALID_CHAR'; // --> true
+  err.message; // --> 'Invalid character in header content ["x-my-header"]'
+}
+```
+
 [`--insecure-http-parser`]: cli.html#cli_insecure_http_parser
 [`--max-http-header-size`]: cli.html#cli_max_http_header_size_size
 [`'checkContinue'`]: #http_event_checkcontinue
diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js
index 5fd462b0194387..d9decf22a018a9 100644
--- a/lib/_http_outgoing.js
+++ b/lib/_http_outgoing.js
@@ -909,6 +909,9 @@ function(err, event) {
   this.destroy(err);
 };
 
+OutgoingMessage.validateHeaderName = validateHeaderName;
+OutgoingMessage.validateHeaderValue = validateHeaderValue;
+
 module.exports = {
   OutgoingMessage
 };
diff --git a/lib/http.js b/lib/http.js
index 55c655763d2647..2b01e1edf5ea19 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -30,6 +30,7 @@ const { ClientRequest } = require('_http_client');
 const { methods } = require('_http_common');
 const { IncomingMessage } = require('_http_incoming');
 const { OutgoingMessage } = require('_http_outgoing');
+const { validateHeaderName, validateHeaderValue } = OutgoingMessage;
 const {
   _connectionListener,
   STATUS_CODES,
@@ -63,6 +64,8 @@ module.exports = {
   Server,
   ServerResponse,
   createServer,
+  validateHeaderName,
+  validateHeaderValue,
   get,
   request
 };
diff --git a/test/parallel/test-http-header-validators.js b/test/parallel/test-http-header-validators.js
new file mode 100644
index 00000000000000..fb23bd4885c766
--- /dev/null
+++ b/test/parallel/test-http-header-validators.js
@@ -0,0 +1,62 @@
+'use strict';
+require('../common');
+const assert = require('assert');
+const { validateHeaderName, validateHeaderValue } = require('http');
+
+// Expected static methods
+isFunc(validateHeaderName, 'validateHeaderName');
+isFunc(validateHeaderValue, 'validateHeaderValue');
+
+// Expected to be useful as static methods
+console.log('validateHeaderName');
+// - when used with valid header names - should not throw
+[
+  'user-agent',
+  'USER-AGENT',
+  'User-Agent',
+  'x-forwarded-for'
+].forEach((name) => {
+  console.log('does not throw for "%s"', name);
+  validateHeaderName(name);
+});
+
+// - when used with invalid header names:
+[
+  'איקס-פורוורד-פור',
+  'x-forwarded-fםr',
+].forEach((name) => {
+  console.log('throws for: "%s"', name.slice(0, 50));
+  assert.throws(
+    () => validateHeaderName(name),
+    { code: 'ERR_INVALID_HTTP_TOKEN' }
+  );
+});
+
+console.log('validateHeaderValue');
+// - when used with valid header values - should not throw
+[
+  ['x-valid', 1],
+  ['x-valid', '1'],
+  ['x-valid', 'string'],
+].forEach(([name, value]) => {
+  console.log('does not throw for "%s"', name);
+  validateHeaderValue(name, value);
+});
+
+// - when used with invalid header values:
+[
+  // [header, value, expectedCode]
+  ['x-undefined', undefined, 'ERR_HTTP_INVALID_HEADER_VALUE'],
+  ['x-bad-char', 'לא תקין', 'ERR_INVALID_CHAR'],
+].forEach(([name, value, code]) => {
+  console.log('throws %s for: "%s: %s"', code, name, value);
+  assert.throws(
+    () => validateHeaderValue(name, value),
+    { code }
+  );
+});
+
+// Misc.
+function isFunc(v, ttl) {
+  assert.ok(v.constructor === Function, `${ttl} is expected to be a function`);
+}