Description
- Version: 10.0.0 and up
- Platform: Darwin CLI-4992 17.5.0 Darwin Kernel Version 17.5.0: Fri Apr 13 19:32:32 PDT 2018; root:xnu-4570.51.2~1/RELEASE_X86_64 x86_64
- Subsystem: net
Beginning with commit 9b7a691, calling .end
a Node.js socket before it is connected will fail to call .destroy
on the socket, so the socket will never emit a close
event.
Here is a minimal repro, provided by @lpinca in dicussion on #19241 (comment):
const net = require('net');
const server = net.createServer();
server.listen(() => {
const socket = net.createConnection(server.address().port);
socket.on('connect', () => console.log('connect'));
socket.on('end', () => console.log('end'));
socket.on('close', () => console.log('close'));
socket.end();
});
A test run illustrating the problem:
$ nvm use 9.11.1
Now using node v9.11.1 (npm v5.6.0)
$ node testSocketEnd.js
connect
end
close
^C
$ nvm use 10.0.0
Now using node v10.0.0 (npm v5.6.0)
$ node testSocketEnd.js
connect
end
^C
$ nvm use 10.4.0
Now using node v10.4.0 (npm v6.1.0)
$ node testSocketEnd.js
connect
end
^C
This occurs because when we fire end before the connect completes (a real-world example is a timeout that fires off an end
on the connecting socket if it is slow) we get the ordering:
-
Socket.end
from client code on timeout (calls down tostream.Duplex.prototype.end
, settingthis.writable
to false andthis._writableState.ending
to true) -
Socket.afterConnect
(setsthis.writable
to true when we connect without error) -
Socket.onReadableStreamEnd
(callsthis.end
) -
Socket.end
(callsstream.Duplex.prototype.end
) -
Writable.prototype.end
(does NOT callendWritable
becausethis._writableState.ending
is already true from the previous call tosocket.end
, sothis.writable
stays true) -
socket.maybeDestroy
(does not callsocket.destroy
becausesocket.writable
istrue
, sothis.destroy
is never called andclose
is never emitted)
While this is not really a problem with 9b7a691, but rather an issue with afterConnect setting writable
without any state checks, commit 9b7a691 removes the behavior that was masking the issue -- previous to that change we were calling destroySoon
, which would call down to destroy
even with .writable
set to true.
This is causing a problem for at least the fairly popular IORedis (redis/ioredis#633), both in theory and in practice, and it seems as though it warrants a look, I'd be surprised if that was the only thing that was depending on those sockets being destroyed and emitting close
.
I'm not sure what to propose as a good fix: I don't immediately see any internal state on the socket that looks appropriate for switching behavior in afterConnect
, although I guess it peeks at the _writableState in a couple of places.