Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -874,8 +874,10 @@ function SocketEmitEndNT(self, _err?) {
}
// A write that was waiting on the native drain can never complete once the
// socket is gone - fail it so 'finish'/destroy are not stuck behind it.
// kclosed, not _err: a native close can carry no error. A half-open 'end'
// also lands here, and its write stays parked because it can still drain.
Comment thread
robobun marked this conversation as resolved.
const pendingWrite = self[kwriteCallback];
if (pendingWrite && (self.destroyed || _err)) {
if (pendingWrite && (self[kclosed] || self.destroyed)) {
self[kwriteCallback] = null;
pendingWrite(_err ?? $ERR_SOCKET_CLOSED());
}
Expand Down
82 changes: 82 additions & 0 deletions test/js/node/tls/node-tls-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2685,3 +2685,85 @@ describe("pauseOnConnect", () => {
}
});
});

it("an accepted socket emits 'close' when a write is the first to see the peer's reset", async () => {
// A send() that fails with the reset consumes the socket error, so the loop then sees a
// plain hangup and the native close carries no error. With the rest of the write still
// waiting for a drain, the socket emitted 'end' and nothing else: no write callback, no
// 'error', no 'close', and the server counted it forever.
//
// The server is a child process so that it can stop polling: it reports the accepted
// socket, then blocks on stdin until this process has reset the connection.
//
// A socket that never closes gives no event to wait for, so a second connection asks.
// Its handshake takes several turns of the server's loop, and the reset socket closes in
// the first of them or not at all. The server reports when the second connection arrives.
const serverScript = `
const tls = require("node:tls");
const fs = require("node:fs");
const events = [];
let accepted;
const server = tls.createServer(${JSON.stringify(COMMON_CERT)}, socket => {
if (accepted) {
server.getConnections((err, connections) => {
console.log(JSON.stringify({ events, connections }));
// Also the first socket, so that this process exits when it never closed.
accepted.destroy();
socket.destroy();
server.close();
});
return;
}
accepted = socket;
socket.on("error", () => events.push("error"));
socket.on("close", hadError => events.push("close:" + hadError));
socket.resume();
fs.writeSync(1, "accepted\\n");
fs.readSync(0, Buffer.alloc(1));
// More than the TLS layer takes once the wire rejects a record, so the rest is parked.
socket.write(Buffer.alloc(1 << 20));
fs.writeSync(1, "wrote\\n");
});
server.listen(0, "127.0.0.1", () => fs.writeSync(1, "port=" + server.address().port + "\\n"));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", serverScript],
env: bunEnv,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
});
// Drain stderr while stdout is scanned, so a child that logs a lot cannot block on it.
const stderrText = proc.stderr.text();
let stdout = "";
let raw: net.Socket | undefined;
let probe: net.Socket | undefined;
let reset = false;
for await (const chunk of proc.stdout) {
stdout += Buffer.from(chunk).toString();
const port = /port=(\d+)/.exec(stdout);
if (port && !raw) {
raw = net.connect(Number(port[1]), "127.0.0.1");
raw.on("error", () => {});
connect({ socket: raw, rejectUnauthorized: false }).on("error", () => {});
}
if (raw && !reset && stdout.includes("accepted\n")) {
reset = true;
raw.resetAndDestroy();
proc.stdin.write("x");
proc.stdin.flush();
}
if (port && !probe && stdout.includes("wrote\n")) {
probe = connect({ port: Number(port[1]), host: "127.0.0.1", rejectUnauthorized: false });
probe.on("error", () => {});
}
}
const [stderr, exitCode] = await Promise.all([stderrText, proc.exited]);
Comment thread
robobun marked this conversation as resolved.
probe?.destroy();
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
// The one connection left is the second one.
expect(JSON.parse(lines[lines.length - 1])).toEqual({ events: ["error", "close:true"], connections: 1 });
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(0);
});
Loading