GeezerVison is showing github.com — http: fix socket re-use races · nodejs/node@f9123eb
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/http.md +3Lines changed: 3 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -239,6 +239,9 @@ added: v0.11.4 | 239 | 239 | An object which contains arrays of sockets currently awaiting use by | 240 | 240 | the agent when `keepAlive` is enabled. Do not modify. | 241 | 241 |
| | 242 | +Sockets in the `freeSockets` list will be automatically destroyed and | | 243 | +removed from the array on `'timeout'`. | | 244 | + | 242 | 245 | ### `agent.getName(options)` | 243 | 246 | <!-- YAML | 244 | 247 | added: v0.11.4 | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_agent.js +34-16Lines changed: 34 additions & 16 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -115,6 +115,12 @@ function Agent(options) { | 115 | 115 | socket[async_id_symbol] = -1; | 116 | 116 | socket._httpMessage = null; | 117 | 117 | this.removeSocket(socket, options); | | 118 | + | | 119 | + const agentTimeout = this.options.timeout || 0; | | 120 | + if (socket.timeout !== agentTimeout) { | | 121 | + socket.setTimeout(agentTimeout); | | 122 | + } | | 123 | + | 118 | 124 | freeSockets.push(socket); | 119 | 125 | } else { | 120 | 126 | // Implementation doesn't want to keep socket alive | @@ -197,12 +203,21 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, | 197 | 203 | this.sockets[name] = []; | 198 | 204 | } | 199 | 205 |
| 200 | | - const freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; | | 206 | + const freeSockets = this.freeSockets[name]; | | 207 | + let socket; | | 208 | + if (freeSockets) { | | 209 | + while (freeSockets.length && freeSockets[0].destroyed) { | | 210 | + freeSockets.shift(); | | 211 | + } | | 212 | + socket = freeSockets.shift(); | | 213 | + if (!freeSockets.length) | | 214 | + delete this.freeSockets[name]; | | 215 | + } | | 216 | + | | 217 | + const freeLen = freeSockets ? freeSockets.length : 0; | 201 | 218 | const sockLen = freeLen + this.sockets[name].length; | 202 | 219 |
| 203 | | - if (freeLen) { | 204 | | - // We have a free socket, so use that. | 205 | | - const socket = this.freeSockets[name].shift(); | | 220 | + if (socket) { | 206 | 221 | // Guard against an uninitialized or user supplied Socket. | 207 | 222 | const handle = socket._handle; | 208 | 223 | if (handle && typeof handle.asyncReset === 'function') { | @@ -211,10 +226,6 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, | 211 | 226 | socket[async_id_symbol] = handle.getAsyncId(); | 212 | 227 | } | 213 | 228 |
| 214 | | - // don't leak | 215 | | - if (!this.freeSockets[name].length) | 216 | | - delete this.freeSockets[name]; | 217 | | - | 218 | 229 | this.reuseSocket(socket, req); | 219 | 230 | setRequestSocket(this, req, socket); | 220 | 231 | this.sockets[name].push(socket); | @@ -309,6 +320,20 @@ function installListeners(agent, s, options) { | 309 | 320 | } | 310 | 321 | s.on('close', onClose); | 311 | 322 |
| | 323 | + function onTimeout() { | | 324 | + debug('CLIENT socket onTimeout'); | | 325 | + | | 326 | + // Destroy if in free list. | | 327 | + // TODO(ronag): Always destroy, even if not in free list. | | 328 | + const sockets = agent.freeSockets; | | 329 | + for (const name of ObjectKeys(sockets)) { | | 330 | + if (sockets[name].includes(s)) { | | 331 | + return s.destroy(); | | 332 | + } | | 333 | + } | | 334 | + } | | 335 | + s.on('timeout', onTimeout); | | 336 | + | 312 | 337 | function onRemove() { | 313 | 338 | // We need this function for cases like HTTP 'upgrade' | 314 | 339 | // (defined by WebSockets) where we need to remove a socket from the | @@ -317,6 +342,7 @@ function installListeners(agent, s, options) { | 317 | 342 | agent.removeSocket(s, options); | 318 | 343 | s.removeListener('close', onClose); | 319 | 344 | s.removeListener('free', onFree); | | 345 | + s.removeListener('timeout', onTimeout); | 320 | 346 | s.removeListener('agentRemove', onRemove); | 321 | 347 | } | 322 | 348 | s.on('agentRemove', onRemove); | @@ -399,14 +425,6 @@ function setRequestSocket(agent, req, socket) { | 399 | 425 | return; | 400 | 426 | } | 401 | 427 | socket.setTimeout(req.timeout); | 402 | | - // Reset timeout after response end | 403 | | - req.once('response', (res) => { | 404 | | - res.once('end', () => { | 405 | | - if (socket.timeout !== agentTimeout) { | 406 | | - socket.setTimeout(agentTimeout); | 407 | | - } | 408 | | - }); | 409 | | - }); | 410 | 428 | } | 411 | 429 |
| 412 | 430 | function emitErrorNT(emitter, err) { | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-agent-timeout-option.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -18,6 +18,6 @@ request.on('socket', mustCall((socket) => { | 18 | 18 |
| 19 | 19 | const listeners = socket.listeners('timeout'); | 20 | 20 |
| 21 | | - strictEqual(listeners.length, 1); | 22 | | - strictEqual(listeners[0], request.timeoutCb); | | 21 | + strictEqual(listeners.length, 2); | | 22 | + strictEqual(listeners[1], request.timeoutCb); | 23 | 23 | })); |
Collapse file Copy file name to clipboard +94Lines changed: 94 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | +const assert = require('assert'); | | 5 | +const http = require('http'); | | 6 | + | | 7 | +{ | | 8 | + // Ensure reuse of successful sockets. | | 9 | + | | 10 | + const agent = new http.Agent({ keepAlive: true }); | | 11 | + | | 12 | + const server = http.createServer((req, res) => { | | 13 | + res.end(); | | 14 | + }); | | 15 | + | | 16 | + server.listen(0, common.mustCall(() => { | | 17 | + let socket; | | 18 | + http.get({ port: server.address().port, agent }) | | 19 | + .on('response', common.mustCall((res) => { | | 20 | + socket = res.socket; | | 21 | + assert(socket); | | 22 | + res.resume(); | | 23 | + socket.on('free', common.mustCall(() => { | | 24 | + http.get({ port: server.address().port, agent }) | | 25 | + .on('response', common.mustCall((res) => { | | 26 | + assert.strictEqual(socket, res.socket); | | 27 | + assert(socket); | | 28 | + agent.destroy(); | | 29 | + server.close(); | | 30 | + })); | | 31 | + })); | | 32 | + })); | | 33 | + })); | | 34 | +} | | 35 | + | | 36 | +{ | | 37 | + // Ensure that timeouted sockets are not reused. | | 38 | + | | 39 | + const agent = new http.Agent({ keepAlive: true, timeout: 50 }); | | 40 | + | | 41 | + const server = http.createServer((req, res) => { | | 42 | + res.end(); | | 43 | + }); | | 44 | + | | 45 | + server.listen(0, common.mustCall(() => { | | 46 | + http.get({ port: server.address().port, agent }) | | 47 | + .on('response', common.mustCall((res) => { | | 48 | + const socket = res.socket; | | 49 | + assert(socket); | | 50 | + res.resume(); | | 51 | + socket.on('free', common.mustCall(() => { | | 52 | + socket.on('timeout', common.mustCall(() => { | | 53 | + http.get({ port: server.address().port, agent }) | | 54 | + .on('response', common.mustCall((res) => { | | 55 | + assert.notStrictEqual(socket, res.socket); | | 56 | + assert.strictEqual(socket.destroyed, true); | | 57 | + agent.destroy(); | | 58 | + server.close(); | | 59 | + })); | | 60 | + })); | | 61 | + })); | | 62 | + })); | | 63 | + })); | | 64 | +} | | 65 | + | | 66 | +{ | | 67 | + // Ensure that destroyed sockets are not reused. | | 68 | + | | 69 | + const agent = new http.Agent({ keepAlive: true }); | | 70 | + | | 71 | + const server = http.createServer((req, res) => { | | 72 | + res.end(); | | 73 | + }); | | 74 | + | | 75 | + server.listen(0, common.mustCall(() => { | | 76 | + let socket; | | 77 | + http.get({ port: server.address().port, agent }) | | 78 | + .on('response', common.mustCall((res) => { | | 79 | + socket = res.socket; | | 80 | + assert(socket); | | 81 | + res.resume(); | | 82 | + socket.on('free', common.mustCall(() => { | | 83 | + socket.destroy(); | | 84 | + http.get({ port: server.address().port, agent }) | | 85 | + .on('response', common.mustCall((res) => { | | 86 | + assert.notStrictEqual(socket, res.socket); | | 87 | + assert(socket); | | 88 | + agent.destroy(); | | 89 | + server.close(); | | 90 | + })); | | 91 | + })); | | 92 | + })); | | 93 | + })); | | 94 | +} |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-client-set-timeout-after-end.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -20,7 +20,7 @@ server.listen(0, () => { | 20 | 20 | const req = get({ agent, port }, (res) => { | 21 | 21 | res.on('end', () => { | 22 | 22 | strictEqual(req.setTimeout(0), req); | 23 | | - strictEqual(socket.listenerCount('timeout'), 0); | | 23 | + strictEqual(socket.listenerCount('timeout'), 1); | 24 | 24 | agent.destroy(); | 25 | 25 | server.close(); | 26 | 26 | }); | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-client-set-timeout.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -42,7 +42,7 @@ server.listen(0, mustCall(() => { | 42 | 42 | })); | 43 | 43 |
| 44 | 44 | req.on('timeout', mustCall(() => { | 45 | | - strictEqual(req.socket.listenerCount('timeout'), 0); | | 45 | + strictEqual(req.socket.listenerCount('timeout'), 1); | 46 | 46 | req.destroy(); | 47 | 47 | })); | 48 | 48 | })); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-client-timeout-option-listeners.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -24,9 +24,9 @@ const options = { | 24 | 24 | server.listen(0, options.host, common.mustCall(() => { | 25 | 25 | options.port = server.address().port; | 26 | 26 | doRequest(common.mustCall((numListeners) => { | 27 | | - assert.strictEqual(numListeners, 1); | | 27 | + assert.strictEqual(numListeners, 2); | 28 | 28 | doRequest(common.mustCall((numListeners) => { | 29 | | - assert.strictEqual(numListeners, 1); | | 29 | + assert.strictEqual(numListeners, 2); | 30 | 30 | server.close(); | 31 | 31 | agent.destroy(); | 32 | 32 | })); | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-client-timeout-option-with-agent.js +2-2Lines changed: 2 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -18,6 +18,6 @@ request.on('socket', mustCall((socket) => { | 18 | 18 |
| 19 | 19 | const listeners = socket.listeners('timeout'); | 20 | 20 |
| 21 | | - strictEqual(listeners.length, 1); | 22 | | - strictEqual(listeners[0], request.timeoutCb); | | 21 | + strictEqual(listeners.length, 2); | | 22 | + strictEqual(listeners[1], request.timeoutCb); | 23 | 23 | })); |
|