-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http: fix socket re-use races #32000
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,6 +120,12 @@ function Agent(options) { | |
socket[async_id_symbol] = -1; | ||
socket._httpMessage = null; | ||
this.removeSocket(socket, options); | ||
|
||
const agentTimeout = this.options.timeout || 0; | ||
if (socket.timeout !== agentTimeout) { | ||
socket.setTimeout(agentTimeout); | ||
} | ||
|
||
freeSockets.push(socket); | ||
} else { | ||
// Implementation doesn't want to keep socket alive | ||
|
@@ -202,12 +208,21 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, | |
this.sockets[name] = []; | ||
} | ||
|
||
const freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; | ||
const freeSockets = this.freeSockets[name]; | ||
let socket; | ||
if (freeSockets) { | ||
while (freeSockets.length && freeSockets[0].destroyed) { | ||
freeSockets.shift(); | ||
} | ||
socket = freeSockets.shift(); | ||
if (!freeSockets.length) | ||
delete this.freeSockets[name]; | ||
} | ||
|
||
const freeLen = freeSockets ? freeSockets.length : 0; | ||
const sockLen = freeLen + this.sockets[name].length; | ||
|
||
if (freeLen) { | ||
// We have a free socket, so use that. | ||
const socket = this.freeSockets[name].shift(); | ||
if (socket) { | ||
// Guard against an uninitialized or user supplied Socket. | ||
const handle = socket._handle; | ||
if (handle && typeof handle.asyncReset === 'function') { | ||
|
@@ -216,10 +231,6 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, | |
socket[async_id_symbol] = handle.getAsyncId(); | ||
} | ||
|
||
// don't leak | ||
if (!this.freeSockets[name].length) | ||
delete this.freeSockets[name]; | ||
|
||
this.reuseSocket(socket, req); | ||
setRequestSocket(this, req, socket); | ||
this.sockets[name].push(socket); | ||
|
@@ -319,6 +330,20 @@ function installListeners(agent, s, options) { | |
} | ||
s.on('close', onClose); | ||
|
||
function onTimeout() { | ||
debug('CLIENT socket onTimeout'); | ||
|
||
// Destroy if in free list. | ||
// TODO(ronag): Always destroy, even if not in free list. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this TODO plan to be completed? From the current usage, it may be a breaking change, which makes |
||
const sockets = agent.freeSockets; | ||
for (const name of ObjectKeys(sockets)) { | ||
lundibundi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (sockets[name].includes(s)) { | ||
return s.destroy(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should remove the destroy socket from freeSockets list immediately to prevent new requests from being sent through this socket. if (sockets[name].includes(s)) {
s.destroy();
return agent.removeSocket(s, options);
} |
||
} | ||
} | ||
} | ||
s.on('timeout', onTimeout); | ||
|
||
function onRemove() { | ||
// We need this function for cases like HTTP 'upgrade' | ||
// (defined by WebSockets) where we need to remove a socket from the | ||
|
@@ -327,6 +352,7 @@ function installListeners(agent, s, options) { | |
agent.removeSocket(s, options); | ||
s.removeListener('close', onClose); | ||
s.removeListener('free', onFree); | ||
s.removeListener('timeout', onTimeout); | ||
s.removeListener('agentRemove', onRemove); | ||
} | ||
s.on('agentRemove', onRemove); | ||
|
@@ -409,14 +435,6 @@ function setRequestSocket(agent, req, socket) { | |
return; | ||
} | ||
socket.setTimeout(req.timeout); | ||
// Reset timeout after response end | ||
req.once('response', (res) => { | ||
res.once('end', () => { | ||
if (socket.timeout !== agentTimeout) { | ||
socket.setTimeout(agentTimeout); | ||
} | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only need to do this if re-using the socket. Move it to where it is put into the free list. |
||
} | ||
|
||
function emitErrorNT(emitter, err) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
'use strict'; | ||
|
||
const common = require('../common'); | ||
const assert = require('assert'); | ||
const http = require('http'); | ||
|
||
{ | ||
// Ensure reuse of successful sockets. | ||
|
||
const agent = new http.Agent({ keepAlive: true }); | ||
|
||
const server = http.createServer((req, res) => { | ||
res.end(); | ||
}); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
let socket; | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
socket = res.socket; | ||
assert(socket); | ||
res.resume(); | ||
socket.on('free', common.mustCall(() => { | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
assert.strictEqual(socket, res.socket); | ||
assert(socket); | ||
agent.destroy(); | ||
server.close(); | ||
})); | ||
})); | ||
})); | ||
})); | ||
ronag marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
{ | ||
// Ensure that timeouted sockets are not reused. | ||
|
||
const agent = new http.Agent({ keepAlive: true, timeout: 50 }); | ||
|
||
const server = http.createServer((req, res) => { | ||
res.end(); | ||
}); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
const socket = res.socket; | ||
assert(socket); | ||
res.resume(); | ||
socket.on('free', common.mustCall(() => { | ||
socket.on('timeout', common.mustCall(() => { | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
assert.notStrictEqual(socket, res.socket); | ||
assert.strictEqual(socket.destroyed, true); | ||
agent.destroy(); | ||
server.close(); | ||
})); | ||
})); | ||
})); | ||
})); | ||
})); | ||
} | ||
|
||
{ | ||
// Ensure that destroyed sockets are not reused. | ||
|
||
const agent = new http.Agent({ keepAlive: true }); | ||
|
||
const server = http.createServer((req, res) => { | ||
res.end(); | ||
}); | ||
|
||
server.listen(0, common.mustCall(() => { | ||
let socket; | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
socket = res.socket; | ||
assert(socket); | ||
res.resume(); | ||
socket.on('free', common.mustCall(() => { | ||
socket.destroy(); | ||
http.get({ port: server.address().port, agent }) | ||
.on('response', common.mustCall((res) => { | ||
assert.notStrictEqual(socket, res.socket); | ||
ronag marked this conversation as resolved.
Show resolved
Hide resolved
|
||
assert(socket); | ||
agent.destroy(); | ||
server.close(); | ||
})); | ||
})); | ||
})); | ||
})); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
freeLen used to include the socket that was removed, however I think that was a mistake as well and it's only used for debug logging.