Skip to content

Commit f5c0e28

Browse files
committed
http2: allow Host in HTTP/2 requests
The HTTP/2 spec allows Host to be used instead of :authority in requests, and this is in fact *preferred* when converting from HTTP/1. We erroneously treated Host as a connection header, thus disallowing it in requests. The patch corrects this, aligning Node.js behaviour with the HTTP/2 spec and with nghttp2: - Treat Host as a single-value header instead of a connection header. - Don't autofill :authority if Host is present. - The compatibility API (request.authority) falls back to using Host if :authority is not present. This is semver-major because requests are no longer guaranteed to have :authority set. An explanatory note was added to the docs. Fixes: #29858 PR-URL: #34664 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de> Reviewed-By: Yongsheng Zhang <zyszys98@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Ricky Zhou <0x19951125@gmail.com>
1 parent 42a3a7f commit f5c0e28

File tree

6 files changed

+125
-14
lines changed

6 files changed

+125
-14
lines changed

doc/api/http2.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!-- YAML
33
added: v8.4.0
44
changes:
5+
- version: REPLACEME
6+
pr-url: https://github.com/nodejs/node/pull/34664
7+
description: Requests with the `host` header (with or without
8+
`:authority`) can now be sent/received.
59
- version: v10.10.0
610
pr-url: https://github.com/nodejs/node/pull/22466
711
description: HTTP/2 is now Stable. Previously, it had been Experimental.
@@ -2530,7 +2534,7 @@ For incoming headers:
25302534
`access-control-max-age`, `access-control-request-method`, `content-encoding`,
25312535
`content-language`, `content-length`, `content-location`, `content-md5`,
25322536
`content-range`, `content-type`, `date`, `dnt`, `etag`, `expires`, `from`,
2533-
`if-match`, `if-modified-since`, `if-none-match`, `if-range`,
2537+
`host`, `if-match`, `if-modified-since`, `if-none-match`, `if-range`,
25342538
`if-unmodified-since`, `last-modified`, `location`, `max-forwards`,
25352539
`proxy-authorization`, `range`, `referer`,`retry-after`, `tk`,
25362540
`upgrade-insecure-requests`, `user-agent` or `x-content-type-options` are
@@ -2909,8 +2913,10 @@ added: v8.4.0
29092913

29102914
* {string}
29112915

2912-
The request authority pseudo header field. It can also be accessed via
2913-
`req.headers[':authority']`.
2916+
The request authority pseudo header field. Because HTTP/2 allows requests
2917+
to set either `:authority` or `host`, this value is derived from
2918+
`req.headers[':authority']` if present. Otherwise, it is derived from
2919+
`req.headers['host']`.
29142920

29152921
#### `request.complete`
29162922
<!-- YAML
@@ -3709,6 +3715,18 @@ following additional properties:
37093715
* `type` {string} Either `'server'` or `'client'` to identify the type of
37103716
`Http2Session`.
37113717

3718+
## Note on `:authority` and `host`
3719+
3720+
HTTP/2 requires requests to have either the `:authority` pseudo-header
3721+
or the `host` header. Prefer `:authority` when constructing an HTTP/2
3722+
request directly, and `host` when converting from HTTP/1 (in proxies,
3723+
for instance).
3724+
3725+
The compatibility API falls back to `host` if `:authority` is not
3726+
present. See [`request.authority`][] for more information. However,
3727+
if you don't use the compatibility API (or use `req.headers` directly),
3728+
you need to implement any fall-back behaviour yourself.
3729+
37123730
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
37133731
[ALPN negotiation]: #http2_alpn_negotiation
37143732
[Compatibility API]: #http2_compatibility_api
@@ -3749,6 +3767,7 @@ following additional properties:
37493767
[`net.Socket.prototype.unref()`]: net.html#net_socket_unref
37503768
[`net.Socket`]: net.html#net_class_net_socket
37513769
[`net.connect()`]: net.html#net_net_connect
3770+
[`request.authority`]: #http2_request_authority
37523771
[`request.socket`]: #http2_request_socket
37533772
[`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
37543773
[`response.end()`]: #http2_response_end_data_encoding_callback

lib/internal/http2/compat.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ const {
5050
kSocket,
5151
kRequest,
5252
kProxySocket,
53-
assertValidPseudoHeader
53+
assertValidPseudoHeader,
54+
getAuthority
5455
} = require('internal/http2/util');
5556
const { _checkIsHttpToken: checkIsHttpToken } = require('_http_common');
5657

@@ -395,7 +396,7 @@ class Http2ServerRequest extends Readable {
395396
}
396397

397398
get authority() {
398-
return this[kHeaders][HTTP2_HEADER_AUTHORITY];
399+
return getAuthority(this[kHeaders]);
399400
}
400401

401402
get scheme() {

lib/internal/http2/core.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const {
118118
assertValidPseudoHeaderResponse,
119119
assertValidPseudoHeaderTrailer,
120120
assertWithinRange,
121+
getAuthority,
121122
getDefaultSettings,
122123
getSessionState,
123124
getSettings,
@@ -1636,7 +1637,7 @@ class ClientHttp2Session extends Http2Session {
16361637
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
16371638

16381639
if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
1639-
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
1640+
if (getAuthority(headers) === undefined)
16401641
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
16411642
if (headers[HTTP2_HEADER_SCHEME] === undefined)
16421643
headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1);
@@ -1667,7 +1668,7 @@ class ClientHttp2Session extends Http2Session {
16671668
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
16681669
stream[kSentHeaders] = headers;
16691670
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
1670-
`${headers[HTTP2_HEADER_AUTHORITY]}`;
1671+
`${getAuthority(headers)}`;
16711672

16721673
// Close the writable side of the stream if options.endStream is set.
16731674
if (options.endStream)
@@ -2459,7 +2460,7 @@ class ServerHttp2Stream extends Http2Stream {
24592460
handle.owner = this;
24602461
this[kInit](id, handle);
24612462
this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
2462-
this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY];
2463+
this[kAuthority] = getAuthority(headers);
24632464
}
24642465

24652466
// True if the remote peer accepts push streams
@@ -2502,7 +2503,7 @@ class ServerHttp2Stream extends Http2Stream {
25022503

25032504
if (headers[HTTP2_HEADER_METHOD] === undefined)
25042505
headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
2505-
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
2506+
if (getAuthority(headers) === undefined)
25062507
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
25072508
if (headers[HTTP2_HEADER_SCHEME] === undefined)
25082509
headers[HTTP2_HEADER_SCHEME] = this[kProtocol];

lib/internal/http2/util.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const {
6161
HTTP2_HEADER_ETAG,
6262
HTTP2_HEADER_EXPIRES,
6363
HTTP2_HEADER_FROM,
64+
HTTP2_HEADER_HOST,
6465
HTTP2_HEADER_IF_MATCH,
6566
HTTP2_HEADER_IF_NONE_MATCH,
6667
HTTP2_HEADER_IF_MODIFIED_SINCE,
@@ -84,7 +85,6 @@ const {
8485
HTTP2_HEADER_HTTP2_SETTINGS,
8586
HTTP2_HEADER_TE,
8687
HTTP2_HEADER_TRANSFER_ENCODING,
87-
HTTP2_HEADER_HOST,
8888
HTTP2_HEADER_KEEP_ALIVE,
8989
HTTP2_HEADER_PROXY_CONNECTION,
9090

@@ -131,6 +131,7 @@ const kSingleValueHeaders = new Set([
131131
HTTP2_HEADER_ETAG,
132132
HTTP2_HEADER_EXPIRES,
133133
HTTP2_HEADER_FROM,
134+
HTTP2_HEADER_HOST,
134135
HTTP2_HEADER_IF_MATCH,
135136
HTTP2_HEADER_IF_MODIFIED_SINCE,
136137
HTTP2_HEADER_IF_NONE_MATCH,
@@ -429,7 +430,6 @@ function isIllegalConnectionSpecificHeader(name, value) {
429430
switch (name) {
430431
case HTTP2_HEADER_CONNECTION:
431432
case HTTP2_HEADER_UPGRADE:
432-
case HTTP2_HEADER_HOST:
433433
case HTTP2_HEADER_HTTP2_SETTINGS:
434434
case HTTP2_HEADER_KEEP_ALIVE:
435435
case HTTP2_HEADER_PROXY_CONNECTION:
@@ -625,12 +625,24 @@ function sessionName(type) {
625625
}
626626
}
627627

628+
function getAuthority(headers) {
629+
// For non-CONNECT requests, HTTP/2 allows either :authority
630+
// or Host to be used equivalently. The first is preferred
631+
// when making HTTP/2 requests, and the latter is preferred
632+
// when converting from an HTTP/1 message.
633+
if (headers[HTTP2_HEADER_AUTHORITY] !== undefined)
634+
return headers[HTTP2_HEADER_AUTHORITY];
635+
if (headers[HTTP2_HEADER_HOST] !== undefined)
636+
return headers[HTTP2_HEADER_HOST];
637+
}
638+
628639
module.exports = {
629640
assertIsObject,
630641
assertValidPseudoHeader,
631642
assertValidPseudoHeaderResponse,
632643
assertValidPseudoHeaderTrailer,
633644
assertWithinRange,
645+
getAuthority,
634646
getDefaultSettings,
635647
getSessionState,
636648
getSettings,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
const assert = require('assert');
7+
const h2 = require('http2');
8+
9+
// Requests using host instead of :authority should be allowed
10+
// and Http2ServerRequest.authority should fall back to host
11+
12+
// :authority should NOT be auto-filled if host is present
13+
14+
const server = h2.createServer();
15+
server.listen(0, common.mustCall(function() {
16+
const port = server.address().port;
17+
server.once('request', common.mustCall(function(request, response) {
18+
const expected = {
19+
':path': '/foobar',
20+
':method': 'GET',
21+
':scheme': 'http',
22+
'host': `localhost:${port}`
23+
};
24+
25+
assert.strictEqual(request.authority, expected.host);
26+
27+
const headers = request.headers;
28+
for (const [name, value] of Object.entries(expected)) {
29+
assert.strictEqual(headers[name], value);
30+
}
31+
32+
const rawHeaders = request.rawHeaders;
33+
for (const [name, value] of Object.entries(expected)) {
34+
const position = rawHeaders.indexOf(name);
35+
assert.notStrictEqual(position, -1);
36+
assert.strictEqual(rawHeaders[position + 1], value);
37+
}
38+
39+
assert(!Object.hasOwnProperty.call(headers, ':authority'));
40+
assert(!Object.hasOwnProperty.call(rawHeaders, ':authority'));
41+
42+
response.on('finish', common.mustCall(function() {
43+
server.close();
44+
}));
45+
response.end();
46+
}));
47+
48+
const url = `http://localhost:${port}`;
49+
const client = h2.connect(url, common.mustCall(function() {
50+
const headers = {
51+
':path': '/foobar',
52+
':method': 'GET',
53+
':scheme': 'http',
54+
'host': `localhost:${port}`
55+
};
56+
const request = client.request(headers);
57+
request.on('end', common.mustCall(function() {
58+
client.close();
59+
}));
60+
request.end();
61+
request.resume();
62+
}));
63+
}));

test/parallel/test-http2-util-headers-list.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ const common = require('../common');
88
if (!common.hasCrypto)
99
common.skip('missing crypto');
1010
const assert = require('assert');
11-
const { mapToHeaders, toHeaderObject } = require('internal/http2/util');
11+
const {
12+
getAuthority,
13+
mapToHeaders,
14+
toHeaderObject
15+
} = require('internal/http2/util');
1216
const { sensitiveHeaders } = require('http2');
1317
const { internalBinding } = require('internal/test/binding');
1418
const {
@@ -34,6 +38,7 @@ const {
3438
HTTP2_HEADER_ETAG,
3539
HTTP2_HEADER_EXPIRES,
3640
HTTP2_HEADER_FROM,
41+
HTTP2_HEADER_HOST,
3742
HTTP2_HEADER_IF_MATCH,
3843
HTTP2_HEADER_IF_MODIFIED_SINCE,
3944
HTTP2_HEADER_IF_NONE_MATCH,
@@ -86,7 +91,6 @@ const {
8691
HTTP2_HEADER_HTTP2_SETTINGS,
8792
HTTP2_HEADER_TE,
8893
HTTP2_HEADER_TRANSFER_ENCODING,
89-
HTTP2_HEADER_HOST,
9094
HTTP2_HEADER_KEEP_ALIVE,
9195
HTTP2_HEADER_PROXY_CONNECTION
9296
} = internalBinding('http2').constants;
@@ -225,6 +229,7 @@ const {
225229
HTTP2_HEADER_ETAG,
226230
HTTP2_HEADER_EXPIRES,
227231
HTTP2_HEADER_FROM,
232+
HTTP2_HEADER_HOST,
228233
HTTP2_HEADER_IF_MATCH,
229234
HTTP2_HEADER_IF_MODIFIED_SINCE,
230235
HTTP2_HEADER_IF_NONE_MATCH,
@@ -289,7 +294,6 @@ const {
289294
HTTP2_HEADER_HTTP2_SETTINGS,
290295
HTTP2_HEADER_TE,
291296
HTTP2_HEADER_TRANSFER_ENCODING,
292-
HTTP2_HEADER_HOST,
293297
HTTP2_HEADER_PROXY_CONNECTION,
294298
HTTP2_HEADER_KEEP_ALIVE,
295299
'Connection',
@@ -327,6 +331,17 @@ assert.throws(
327331
mapToHeaders({ te: 'trailers' });
328332
mapToHeaders({ te: ['trailers'] });
329333

334+
// HTTP/2 encourages use of Host instead of :authority when converting
335+
// from HTTP/1 to HTTP/2, so we no longer disallow it.
336+
// Refs: https://github.com/nodejs/node/issues/29858
337+
mapToHeaders({ [HTTP2_HEADER_HOST]: 'abc' });
338+
339+
// If both are present, the latter has priority
340+
assert.strictEqual(getAuthority({
341+
[HTTP2_HEADER_AUTHORITY]: 'abc',
342+
[HTTP2_HEADER_HOST]: 'def'
343+
}), 'abc');
344+
330345

331346
{
332347
const rawHeaders = [

0 commit comments

Comments
 (0)