Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.
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
61 changes: 61 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,64 @@ 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.
const serverScript = `
const tls = require("node:tls");
const fs = require("node:fs");
const events = [];
const server = tls.createServer(${JSON.stringify(COMMON_CERT)}, socket => {
socket.on("error", () => events.push("error"));
socket.on("close", hadError => {
events.push("close:" + hadError);
server.getConnections((err, connections) => {
console.log(JSON.stringify({ events, connections }));
server.close();
});
});
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));
});
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",
});
let stdout = "";
let raw: 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();
}
}
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const lines = stdout.trim().split("\n");
expect(JSON.parse(lines[lines.length - 1])).toEqual({ events: ["error", "close:true"], connections: 0 });
expect(exitCode).toBe(0);
});
Loading