Skip to content

Commit 014c725

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: nodejs#29858
1 parent df17fcd commit 014c725

File tree

6 files changed

+131
-14
lines changed

6 files changed

+131
-14
lines changed

doc/api/http2.md

+28-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: REPLACEME
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
@@ -2908,8 +2912,9 @@ added: v8.4.0
29082912

29092913
* {string}
29102914

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

29142919
#### `request.complete`
29152920
<!-- YAML
@@ -3708,6 +3713,25 @@ following additional properties:
37083713
* `type` {string} Either `'server'` or `'client'` to identify the type of
37093714
`Http2Session`.
37103715

3716+
## Note on `:authority` and `host`
3717+
3718+
HTTP/2 requires requests to have either the `:authority` pseudo-header
3719+
or the `host` header. Using `:authority` should be preferred when
3720+
constructing an HTTP/2 request directly, and `host` should be
3721+
preferred when converting from HTTP/1 (in proxies, for instance).
3722+
3723+
Historically, Node.js required all requests to have the `:authority`
3724+
pseudo-header and rejected with the `host` header present.
3725+
This has been corrected; the `host` header is now allowed, and the
3726+
only requirement is for either `host` or `:authority` to be
3727+
present (or both). The same validations from `:authority` extend
3728+
to `host`.
3729+
3730+
The compatibility API falls back to `host` if `:authority` is not
3731+
present, see [`request.authority`][]. However, if you don't use the
3732+
compatibility API (or use `req.headers` directly), you need to
3733+
implement any fall-back behaviour yourself.
3734+
37113735
[ALPN Protocol ID]: https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
37123736
[ALPN negotiation]: #http2_alpn_negotiation
37133737
[Compatibility API]: #http2_compatibility_api
@@ -3748,6 +3772,7 @@ following additional properties:
37483772
[`net.Socket.prototype.unref()`]: net.html#net_socket_unref
37493773
[`net.Socket`]: net.html#net_class_net_socket
37503774
[`net.connect()`]: net.html#net_net_connect
3775+
[`request.authority`]: #http2_request_authority
37513776
[`request.socket`]: #http2_request_socket
37523777
[`request.socket.getPeerCertificate()`]: tls.html#tls_tlssocket_getpeercertificate_detailed
37533778
[`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,
@@ -1633,7 +1634,7 @@ class ClientHttp2Session extends Http2Session {
16331634
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
16341635

16351636
if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
1636-
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
1637+
if (getAuthority(headers) === undefined)
16371638
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
16381639
if (headers[HTTP2_HEADER_SCHEME] === undefined)
16391640
headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1);
@@ -1664,7 +1665,7 @@ class ClientHttp2Session extends Http2Session {
16641665
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
16651666
stream[kSentHeaders] = headers;
16661667
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
1667-
`${headers[HTTP2_HEADER_AUTHORITY]}`;
1668+
`${getAuthority(headers)}`;
16681669

16691670
// Close the writable side of the stream if options.endStream is set.
16701671
if (options.endStream)
@@ -2456,7 +2457,7 @@ class ServerHttp2Stream extends Http2Stream {
24562457
handle.owner = this;
24572458
this[kInit](id, handle);
24582459
this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
2459-
this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY];
2460+
this[kAuthority] = getAuthority(headers);
24602461
}
24612462

24622463
// True if the remote peer accepts push streams
@@ -2499,7 +2500,7 @@ class ServerHttp2Stream extends Http2Stream {
24992500

25002501
if (headers[HTTP2_HEADER_METHOD] === undefined)
25012502
headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
2502-
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
2503+
if (getAuthority(headers) === undefined)
25032504
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
25042505
if (headers[HTTP2_HEADER_SCHEME] === undefined)
25052506
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,
@@ -418,7 +419,6 @@ function isIllegalConnectionSpecificHeader(name, value) {
418419
switch (name) {
419420
case HTTP2_HEADER_CONNECTION:
420421
case HTTP2_HEADER_UPGRADE:
421-
case HTTP2_HEADER_HOST:
422422
case HTTP2_HEADER_HTTP2_SETTINGS:
423423
case HTTP2_HEADER_KEEP_ALIVE:
424424
case HTTP2_HEADER_PROXY_CONNECTION:
@@ -614,12 +614,24 @@ function sessionName(type) {
614614
}
615615
}
616616

617+
function getAuthority(headers) {
618+
// For non-CONNECT requests, HTTP/2 allows either :authority
619+
// or Host to be used equivalently. The first is preferred
620+
// when making HTTP/2 requests, and the latter is preferred
621+
// when converting from an HTTP/1 message.
622+
if (headers[HTTP2_HEADER_AUTHORITY] !== undefined)
623+
return headers[HTTP2_HEADER_AUTHORITY];
624+
if (headers[HTTP2_HEADER_HOST] !== undefined)
625+
return headers[HTTP2_HEADER_HOST];
626+
}
627+
617628
module.exports = {
618629
assertIsObject,
619630
assertValidPseudoHeader,
620631
assertValidPseudoHeaderResponse,
621632
assertValidPseudoHeaderTrailer,
622633
assertWithinRange,
634+
getAuthority,
623635
getDefaultSettings,
624636
getSessionState,
625637
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 hos
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)