Skip to content

Commit

Permalink
http: add separateHeadersValues option to request and createServer.
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Jan 4, 2022
1 parent ad747a8 commit 01b1f01
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 27 deletions.
60 changes: 59 additions & 1 deletion doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ changes:
**Default:** `'lifo'`.
* `timeout` {number} Socket timeout in milliseconds.
This will set the timeout when the socket is created.
* `separateHeadersValues` {boolean} If set to `true`, the request and the
response headers will never be squashed.
All request headers will be sent multiple times if the header
value was set to an array or set multiple times.
All response headers will always be returned as a string if the
header has been received once or as an array of strings if the header has
been received multiple times.
In both cases headers matching is case insensitive.

`options` in [`socket.connect()`][] are also supported.

Expand Down Expand Up @@ -983,6 +991,10 @@ or
request.setHeader('Cookie', ['type=ninja', 'language=javascript']);
```

If `separateHeadersValues` was set to `true` when the request was created, then
using this method will always append a new value to the list of values for this
header.

### `request.setNoDelay([noDelay])`

<!-- YAML
Expand Down Expand Up @@ -1629,6 +1641,10 @@ Reads out a header that's already been queued but not sent to the client.
The name is case-insensitive. The type of the return value depends
on the arguments provided to [`response.setHeader()`][].

If `separateHeadersValues` was set to `true` when the server was created, then
the return value is always a string if the header has been set once or
an array of strings if the header has been set multiple times.

```js
response.setHeader('Content-Type', 'text/html');
response.setHeader('Content-Length', Buffer.byteLength(body));
Expand All @@ -1652,6 +1668,9 @@ added: v7.7.0
Returns an array containing the unique names of the current outgoing headers.
All header names are lowercase.

If `separateHeadersValues` was set to `true` when the server was created, an
header might be present in the list more than once.

```js
response.setHeader('Foo', 'bar');
response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);
Expand Down Expand Up @@ -1679,6 +1698,10 @@ prototypically inherit from the JavaScript `Object`. This means that typical
`Object` methods such as `obj.toString()`, `obj.hasOwnProperty()`, and others
are not defined and _will not work_.

If `separateHeadersValues` was set to `true` when the server was created, then
the values are always a string if the header will be sent once or
an array of strings if the header will be sent multiple times.

```js
response.setHeader('Foo', 'bar');
response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);
Expand Down Expand Up @@ -1788,6 +1811,10 @@ When headers have been set with [`response.setHeader()`][], they will be merged
with any headers passed to [`response.writeHead()`][], with the headers passed
to [`response.writeHead()`][] given precedence.

If `separateHeadersValues` was set to `true` when the request was created, then
using this method will always append a new value to the list of values for this
header.

```js
// Returns content-type = text/plain
const server = http.createServer((req, res) => {
Expand Down Expand Up @@ -2551,11 +2578,15 @@ added: v0.4.0
-->

* `name` {string} Name of header
* Returns {string | undefined}
* Returns {string | string\[] | undefined}

Gets the value of HTTP header with the given name. If such a name doesn't
exist in message, it will be `undefined`.

If `separateHeadersValues` was set to `true` when the request or the server
were created, then the return value is always a string if the header has been
set once or an array of strings if the header has been set multiple times.

### `outgoingMessage.getHeaderNames()`

<!-- YAML
Expand All @@ -2567,6 +2598,9 @@ added: v8.0.0
Returns an array of names of headers of the outgoing outgoingMessage. All
names are lowercase.

If `separateHeadersValues` was set to `true` when the request or the server
were created, an header might be present in the list more than once.

### `outgoingMessage.getHeaders()`

<!-- YAML
Expand Down Expand Up @@ -2594,6 +2628,10 @@ const headers = outgoingMessage.getHeaders();
// headers === { foo: 'bar', 'set-cookie': ['foo=bar', 'bar=baz'] }
```

If `separateHeadersValues` was set to `true` when the request or the server
were created, then the values are always a string if the header will be sent
once or an array of strings if the header will be sent multiple times.

### `outgoingMessage.hasHeader(name)`

<!-- YAML
Expand Down Expand Up @@ -2659,6 +2697,10 @@ added: v0.4.0

Sets a single header value for the header object.

If `separateHeadersValues` was set to `true` when the request or the server
were created, then using this method will always append a new value to the list
of values for this header.

### `outgoingMessage.setTimeout(msesc[, callback])`

<!-- YAML
Expand Down Expand Up @@ -2865,6 +2907,14 @@ changes:
[`--max-http-header-size`][] for requests received by this server, i.e.
the maximum length of request headers in bytes.
**Default:** 16384 (16 KB).
* `separateHeadersValues` {boolean} If set to `true`, incoming requests and
server responses headers will never be squashed.
All incoming requests headers will always be returned as a string if the
header has been received once or as an array of strings if the header has
been received multiple times.
All server responses headers will be sent multiple times if the header
value was set to an array or set multiple times.
In both cases headers matching is case insensitive.

* `requestListener` {Function}

Expand Down Expand Up @@ -3072,6 +3122,14 @@ changes:
`hostname`. Valid values are `4` or `6`. When unspecified, both IP v4 and
v6 will be used.
* `headers` {Object} An object containing request headers.
* `separateHeadersValues` {boolean} If set to `true`, the request and the
response headers will never be squashed.
All request headers will be sent multiple times if the header
value was set to an array or set multiple times.
All response headers will always be returned as a string if the
header has been received once or as an array of strings if the header has
been received multiple times.
In both cases headers matching is case insensitive.
* `hints` {number} Optional [`dns.lookup()` hints][].
* `host` {string} A domain name or IP address of the server to issue the
request to. **Default:** `'localhost'`.
Expand Down
11 changes: 10 additions & 1 deletion lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
const {
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayIsArray,
ArrayPrototypePop,
ArrayPrototypePush,
ArrayPrototypeShift,
Expand Down Expand Up @@ -60,6 +61,7 @@ const {
validateOneOf,
validateString,
} = require('internal/validators');
const { kSeparateHeadersValues } = require('_http_incoming');

const kOnKeylog = Symbol('onkeylog');
const kRequestOptions = Symbol('requestOptions');
Expand Down Expand Up @@ -344,7 +346,14 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) {

function calculateServerName(options, req) {
let servername = options.host;
const hostHeader = req.getHeader('host');
let hostHeader = req.getHeader('host');

// If the separateHeadersValues is active, the header might be specified
// multiple times. In that case we choose the last value.
if(req[kSeparateHeadersValues] && ArrayIsArray(hostHeader)) {
hostHeader = hostHeader[hostHeader.length - 1];
}

if (hostHeader) {
validateString(hostHeader, 'options.headers.host');

Expand Down
4 changes: 4 additions & 0 deletions lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const assert = require('internal/assert');
const { once } = require('internal/util');
const {
_checkIsHttpToken: checkIsHttpToken,
kSeparateHeadersValues,
freeParser,
parsers,
HTTPParser,
Expand Down Expand Up @@ -195,6 +196,7 @@ function ClientRequest(input, options, cb) {
if (maxHeaderSize !== undefined)
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
this.maxHeaderSize = maxHeaderSize;
this[kSeparateHeadersValues] = Boolean(options.separateHeadersValues);

const insecureHTTPParser = options.insecureHTTPParser;
if (insecureHTTPParser !== undefined &&
Expand Down Expand Up @@ -726,6 +728,8 @@ function tickOnSocket(req, socket) {
0);
parser.socket = socket;
parser.outgoing = req;
parser[kSeparateHeadersValues] = req[kSeparateHeadersValues];

req.parser = parser;

socket.parser = parser;
Expand Down
6 changes: 4 additions & 2 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const { getOptionValue } = require('internal/options');
const insecureHTTPParser = getOptionValue('--insecure-http-parser');

const FreeList = require('internal/freelist');
const incoming = require('_http_incoming');
const {
kSeparateHeadersValues,
IncomingMessage,
readStart,
readStop
} = incoming;
} = require('_http_incoming');

let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
debug = fn;
Expand Down Expand Up @@ -101,6 +101,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
incoming.httpVersion = `${versionMajor}.${versionMinor}`;
incoming.url = url;
incoming.upgrade = upgrade;
incoming[kSeparateHeadersValues] = parser[kSeparateHeadersValues];

if (socket) {
debug('requestTimeout timer moved to req');
Expand Down Expand Up @@ -272,6 +273,7 @@ module.exports = {
freeParser,
methods,
parsers,
kSeparateHeadersValues,
kIncomingMessage,
kRequestTimeout,
HTTPParser,
Expand Down
47 changes: 41 additions & 6 deletions lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
'use strict';

const {
ArrayIsArray,
ObjectDefineProperty,
ObjectSetPrototypeOf,
StringPrototypeCharCodeAt,
Expand All @@ -34,6 +35,7 @@ const { Readable, finished } = require('stream');

const kHeaders = Symbol('kHeaders');
const kHeadersCount = Symbol('kHeadersCount');
const kSeparateHeadersValues = Symbol('kSeparateHeadersValues');
const kTrailers = Symbol('kTrailers');
const kTrailersCount = Symbol('kTrailersCount');

Expand Down Expand Up @@ -112,8 +114,14 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headers', {
const src = this.rawHeaders;
const dst = this[kHeaders];

for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
if (this[kSeparateHeadersValues]) {
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLineSeparate(src[n + 0], src[n + 1], dst);
}
} else {
for (let n = 0; n < this[kHeadersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
}
}
}
return this[kHeaders];
Expand All @@ -131,8 +139,14 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailers', {
const src = this.rawTrailers;
const dst = this[kTrailers];

for (let n = 0; n < this[kTrailersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
if (this[kSeparateHeadersValues]) {
for (let n = 0; n < this[kTrailersCount]; n += 2) {
this._addHeaderLineSeparate(src[n + 0], src[n + 1], dst);
}
} else {
for (let n = 0; n < this[kTrailersCount]; n += 2) {
this._addHeaderLine(src[n + 0], src[n + 1], dst);
}
}
}
return this[kTrailers];
Expand Down Expand Up @@ -213,8 +227,14 @@ function _addHeaderLines(headers, n) {
}

if (dest) {
for (let i = 0; i < n; i += 2) {
this._addHeaderLine(headers[i], headers[i + 1], dest);
if (this[kSeparateHeadersValues]) {
for (let i = 0; i < n; i += 2) {
this._addHeaderLineSeparate(headers[i], headers[i + 1], dest);
}
} else {
for (let i = 0; i < n; i += 2) {
this._addHeaderLine(headers[i], headers[i + 1], dest);
}
}
}
}
Expand Down Expand Up @@ -361,6 +381,20 @@ function _addHeaderLine(field, value, dest) {
}
}

// Add the given (field, value) pair to the message, without joining values as
// in _addHeaderLine. The headers values are always arrays.
IncomingMessage.prototype._addHeaderLineSeparate = _addHeaderLineSeparate;
function _addHeaderLineSeparate(field, value, dest) {
field = StringPrototypeToLowerCase(field);

if(ArrayIsArray(dest[field])) {
dest[field].push(value);
} else if (dest[field] !== undefined) {
dest[field] = [dest[field], value];
} else {
dest[field] = value;
}
}

// Call this instead of resume() if we want to just
// dump all the data to /dev/null
Expand All @@ -385,6 +419,7 @@ function onError(self, error, cb) {
}

module.exports = {
kSeparateHeadersValues,
IncomingMessage,
readStart,
readStop
Expand Down
Loading

0 comments on commit 01b1f01

Please sign in to comment.