Skip to content
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: remove stale timeout listeners #9440

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/_http_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,13 @@ function tickOnSocket(req, socket) {
socket.on('close', socketCloseListener);

if (req.timeout) {
socket.once('timeout', () => req.emit('timeout'));
const emitRequestTimeout = () => req.emit('timeout');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to derail this PR which is desperately needed, but isn't this section very similar to the existing ClientRequest.prototype.setTimeout method? Would just calling req.setTimeout(req.timeout) at this point achieve almost the same behavior?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought along the same lines when I found this bug. The code implementing this feature didn't look like I expected, but I just wanted to make a small, conceptually simple fix to get it merged as quickly as possible.

It think your suggestion would work. What does @evanlucas say?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reused your unit test code and took a stab at the approach described above in #9831

The primary difference is that any timeout set applies from the time a socket is assigned to a request, i.e. it includes socket pre-connect timeouts. In existing code, if a timeout is set via ClientRequest.prototype.setTimeout, it is "reapplied" and starts counting only from the time the socket connects. Perhaps this would count as an API change.

In other HTTP libraries, the connect-timeout and the transaction-timeout are two separate parameters, e.g. in curl https://curl.haxx.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html and https://curl.haxx.se/libcurl/c/CURLOPT_TIMEOUT.html

socket.once('timeout', emitRequestTimeout);
req.on('response', (r) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, I wonder if the response event is emitted in all cases if the request is aborted...will have to dig into that

Copy link
Author

@karlbohlmark karlbohlmark Nov 3, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think it is, but I think the socket is destroyed, which solves the cleanup issue in that case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you call the argument res for consistency/parity? Is there a reason to use .on instead of .once? The timeout listener is installed with .once().

r.on('end', () => {
socket.removeListener('timeout', emitRequestTimeout);
});
});
}
req.emit('socket', socket);
}
Expand Down
55 changes: 55 additions & 0 deletions test/parallel/test-http-client-timeout-option-listeners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';
const common = require('../common');
const http = require('http');
const assert = require('assert');

const agent = new http.Agent({ keepAlive: true });

const server = http.createServer((req, res) => {
res.end('');
});

const options = {
agent,
method: 'GET',
port: undefined,
host: common.localhostIPv4,
path: '/',
timeout: 100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you wrap the timeout in common.platformTimeout()

};

process.on('unhandledRejection', function(reason) {
common.fail('A promise rejection was unhandled: ' + reason);
});

server.listen(0, options.host, (e) => {
options.port = server.address().port;

doRequest().then(doRequest)
.then((numListeners) => {
assert.strictEqual(numListeners, 1);
server.close();
agent.destroy();
});
});

function doRequest() {
return new Promise((resolve, reject) => {
http.request(options, (response) => {
const sockets = agent.sockets[`${options.host}:${options.port}:`];
if (sockets.length !== 1) {
reject(new Error('Only one socket should be created'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a return here?

}
const socket = sockets[0];
const timeoutEvent = socket._events.timeout;
let numListeners = 0;
if (Array.isArray(timeoutEvent)) {
numListeners = timeoutEvent.length;
} else if (timeoutEvent) {
numListeners = 1;
}
response.resume();
response.on('end', () => resolve(numListeners));
}).end();
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd write the test in simple callback style. Less chance of accidentally swallowing errors and easier to debug because you can simply assert.strictEqual(sockets.length, 1) instead of calling reject().