Skip to content

Commit 33ce9a6

Browse files
apapirovskijasnell
authored andcommitted
http: relax requirements on upgrade listener
The http spec does not say anything about Upgrade headers making protocol switch mandatory but Node.js implements them as if they are. Relax the requirements to only destroy the socket if no upgrade listener exists on the client when status code is 101. PR-URL: #19981 Fixes: #11552 Refs: https://tools.ietf.org/html/rfc7230#section-6.7 Reviewed-By: Luigi Pinca <luigipinca@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 6d1c3e5 commit 33ce9a6

File tree

7 files changed

+43
-40
lines changed

7 files changed

+43
-40
lines changed

doc/api/http.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,8 +471,9 @@ added: v0.1.94
471471
* `head` {Buffer}
472472

473473
Emitted each time a server responds to a request with an upgrade. If this
474-
event is not being listened for, clients receiving an upgrade header will have
475-
their connections closed.
474+
event is not being listened for and the response status code is 101 Switching
475+
Protocols, clients receiving an upgrade header will have their connections
476+
closed.
476477

477478
A client server pair demonstrating how to listen for the `'upgrade'` event.
478479

@@ -886,16 +887,20 @@ per connection (in the case of HTTP Keep-Alive connections).
886887
### Event: 'upgrade'
887888
<!-- YAML
888889
added: v0.1.94
890+
changes:
891+
- version: REPLACEME
892+
pr-url: REPLACEME
893+
description: Not listening to this event no longer causes the socket
894+
to be destroyed if a client sends an Upgrade header.
889895
-->
890896

891897
* `request` {http.IncomingMessage} Arguments for the HTTP request, as it is in
892898
the [`'request'`][] event
893899
* `socket` {net.Socket} Network socket between the server and client
894900
* `head` {Buffer} The first packet of the upgraded stream (may be empty)
895901

896-
Emitted each time a client requests an HTTP upgrade. If this event is not
897-
listened for, then clients requesting an upgrade will have their connections
898-
closed.
902+
Emitted each time a client requests an HTTP upgrade. Listening to this event
903+
is optional and clients cannot insist on a protocol change.
899904

900905
After this event is emitted, the request's socket will not have a `'data'`
901906
event listener, meaning it will need to be bound in order to handle data

lib/_http_client.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ function socketOnData(d) {
426426
req.socket._hadError = true;
427427
req.emit('error', ret);
428428
} else if (parser.incoming && parser.incoming.upgrade) {
429-
// Upgrade or CONNECT
429+
// Upgrade (if status code 101) or CONNECT
430430
var bytesParsed = ret;
431431
var res = parser.incoming;
432432
req.res = res;
@@ -453,7 +453,7 @@ function socketOnData(d) {
453453
req.emit(eventName, res, socket, bodyHead);
454454
req.emit('close');
455455
} else {
456-
// Got Upgrade header or CONNECT method, but have no handler.
456+
// Requested Upgrade or used CONNECT method, but have no handler.
457457
socket.destroy();
458458
}
459459
} else if (parser.incoming && parser.incoming.complete &&
@@ -492,6 +492,10 @@ function parserOnIncomingClient(res, shouldKeepAlive) {
492492
}
493493
req.res = res;
494494

495+
// Skip body and treat as Upgrade.
496+
if (res.upgrade)
497+
return 2;
498+
495499
// Responses to CONNECT request is handled as Upgrade.
496500
const method = req.method;
497501
if (method === 'CONNECT') {

lib/_http_common.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
8383
parser.incoming.httpVersionMinor = versionMinor;
8484
parser.incoming.httpVersion = `${versionMajor}.${versionMinor}`;
8585
parser.incoming.url = url;
86+
parser.incoming.upgrade = upgrade;
8687

8788
var n = headers.length;
8889

@@ -101,19 +102,6 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
101102
parser.incoming.statusMessage = statusMessage;
102103
}
103104

104-
if (upgrade && parser.outgoing !== null && !parser.outgoing.upgrading) {
105-
// The client made non-upgrade request, and server is just advertising
106-
// supported protocols.
107-
//
108-
// See RFC7230 Section 6.7
109-
upgrade = false;
110-
}
111-
112-
parser.incoming.upgrade = upgrade;
113-
114-
if (upgrade)
115-
return 2; // Skip body and treat as Upgrade.
116-
117105
return parser.onIncoming(parser.incoming, shouldKeepAlive);
118106
}
119107

lib/_http_outgoing.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ function OutgoingMessage() {
9898
this.writable = true;
9999

100100
this._last = false;
101-
this.upgrading = false;
102101
this.chunkedEncoding = false;
103102
this.shouldKeepAlive = true;
104103
this.useChunkedEncodingByDefault = true;
@@ -304,7 +303,6 @@ function _storeHeader(firstLine, headers) {
304303
// in the case of response it is: 'HTTP/1.1 200 OK\r\n'
305304
var state = {
306305
connection: false,
307-
connUpgrade: false,
308306
contLen: false,
309307
te: false,
310308
date: false,
@@ -366,10 +364,6 @@ function _storeHeader(firstLine, headers) {
366364
}
367365
}
368366

369-
// Are we upgrading the connection?
370-
if (state.connUpgrade && state.upgrade)
371-
this.upgrading = true;
372-
373367
// Date header
374368
if (this.sendDate && !state.date) {
375369
state.header += 'Date: ' + utcDate() + CRLF;
@@ -460,8 +454,6 @@ function matchConnValue(self, state, value) {
460454
while (m) {
461455
if (m[0].length === 5)
462456
sawClose = true;
463-
else
464-
state.connUpgrade = true;
465457
m = RE_CONN_VALUES.exec(value);
466458
}
467459
if (sawClose)

lib/_http_server.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,14 +530,14 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
530530
parser = null;
531531

532532
var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
533-
if (server.listenerCount(eventName) > 0) {
533+
if (eventName === 'upgrade' || server.listenerCount(eventName) > 0) {
534534
debug('SERVER have listener for %s', eventName);
535535
var bodyHead = d.slice(bytesParsed, d.length);
536536

537537
socket.readableFlowing = null;
538538
server.emit(eventName, req, socket, bodyHead);
539539
} else {
540-
// Got upgrade header or CONNECT method, but have no handler.
540+
// Got CONNECT method, but have no handler.
541541
socket.destroy();
542542
}
543543
}
@@ -592,6 +592,13 @@ function resOnFinish(req, res, socket, state, server) {
592592
function parserOnIncoming(server, socket, state, req, keepAlive) {
593593
resetSocketTimeout(server, socket, state);
594594

595+
if (req.upgrade) {
596+
req.upgrade = req.method === 'CONNECT' ||
597+
server.listenerCount('upgrade') > 0;
598+
if (req.upgrade)
599+
return 2;
600+
}
601+
595602
state.incoming.push(req);
596603

597604
// If the writable end isn't consuming, then stop reading

test/parallel/test-http-upgrade-advertise.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const tests = [
88
{ headers: {}, expected: 'regular' },
99
{ headers: { upgrade: 'h2c' }, expected: 'regular' },
1010
{ headers: { connection: 'upgrade' }, expected: 'regular' },
11-
{ headers: { connection: 'upgrade', upgrade: 'h2c' }, expected: 'upgrade' }
11+
{ headers: { connection: 'upgrade', upgrade: 'h2c' }, expected: 'upgrade' },
12+
{ headers: { connection: 'upgrade', upgrade: 'h2c' }, expected: 'destroy' },
13+
{ headers: { connection: 'upgrade', upgrade: 'h2c' }, expected: 'regular' },
1214
];
1315

1416
function fire() {
@@ -32,10 +34,17 @@ function fire() {
3234
done('regular');
3335
});
3436

35-
req.on('upgrade', function onUpgrade(res, socket) {
36-
socket.destroy();
37-
done('upgrade');
38-
});
37+
if (test.expected === 'destroy') {
38+
req.on('socket', () => req.socket.on('close', () => {
39+
server.removeAllListeners('upgrade');
40+
done('destroy');
41+
}));
42+
} else {
43+
req.on('upgrade', function onUpgrade(res, socket) {
44+
socket.destroy();
45+
done('upgrade');
46+
});
47+
}
3948

4049
req.end();
4150
}

test/parallel/test-http-upgrade-server.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ function test_upgrade_with_listener() {
121121
/*-----------------------------------------------
122122
connection: Upgrade, no listener
123123
-----------------------------------------------*/
124-
let test_upgrade_no_listener_ended = false;
125-
126124
function test_upgrade_no_listener() {
127125
const conn = net.createConnection(server.address().port);
128126
conn.setEncoding('utf8');
@@ -135,8 +133,9 @@ function test_upgrade_no_listener() {
135133
'\r\n');
136134
});
137135

138-
conn.on('end', function() {
139-
test_upgrade_no_listener_ended = true;
136+
conn.once('data', (data) => {
137+
assert.strictEqual(typeof data, 'string');
138+
assert.strictEqual(data.substr(0, 12), 'HTTP/1.1 200');
140139
conn.end();
141140
});
142141

@@ -182,5 +181,4 @@ server.listen(0, function() {
182181
process.on('exit', function() {
183182
assert.strictEqual(3, requests_recv);
184183
assert.strictEqual(3, requests_sent);
185-
assert.ok(test_upgrade_no_listener_ended);
186184
});

0 commit comments

Comments
 (0)