Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 3 additions & 4 deletions packages/bun-usockets/src/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -724,12 +724,11 @@
}

void us_internal_socket_raw_shutdown(struct us_socket_t *s) {
/* Todo: should we emit on_close if calling shutdown on an already half-closed socket?
* We need more states in that case, we need to track RECEIVED_FIN
* so far, the app has to track this and call close as needed */
if (!us_socket_is_closed(s) && us_internal_poll_type(&s->p) != POLL_TYPE_SOCKET_SHUT_DOWN) {
us_internal_poll_set_type(&s->p, POLL_TYPE_SOCKET_SHUT_DOWN);
us_poll_change(&s->p, s->group->loop, us_poll_events(&s->p) & LIBUS_SOCKET_READABLE);
/* After read_eof the poll may be at 0 events; re-arm READABLE so the SHUT_DOWN eof branch closes us. */
us_poll_change(&s->p, s->group->loop,
s->read_eof ? LIBUS_SOCKET_READABLE : (us_poll_events(&s->p) & LIBUS_SOCKET_READABLE));

Check warning on line 731 in packages/bun-usockets/src/socket.c

View check run for this annotation

Claude / Claude Code Review

read_eof invariant comment in internal.h is now stale

The `read_eof` declaration comment at `internal.h:317` states "readable interest is never re-added", but this change now explicitly re-arms `READABLE` when `s->read_eof` is set — that clause is no longer true. The operative guarantee ("on_end never re-fires") still holds because the socket is `SHUT_DOWN` and loop.c's eof path closes before dispatch; consider rewording the declaration to the invariant that actually holds, e.g. "on_end never re-fires; READABLE is only re-armed once SHUT_DOWN so th
Comment thread
robobun marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
bsd_shutdown_socket(us_poll_fd((struct us_poll_t *) s));
#ifdef LIBUS_USE_KQUEUE
if (!(us_poll_events(&s->p) & LIBUS_SOCKET_READABLE)) {
Expand Down
56 changes: 56 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3827,3 +3827,59 @@ describe("allowHalfOpen socket whose peer resets behind pending writes", () => {
expect(endCount).toBe(1);
});
});

describe("allowHalfOpen socket shut down after the peer's FIN", () => {
// Once end() has been delivered the poll drops readable interest and, with
// nothing queued, settles with no events armed. shutdown() must re-arm it so
// the fully-closed state is delivered; unfixed, the libuv backend stranded.
it("closes when shutdown() runs after the poll has settled", async () => {
const ended = Promise.withResolvers<Socket>();
const drained = Promise.withResolvers<void>();
const closed = Promise.withResolvers<void>();

using server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
allowHalfOpen: true,
socket: {
open() {},
data() {},
end: s => ended.resolve(s),
// The EOF path arms one writable wakeup; this is the last event the
// socket emits before it goes idle.
drain: () => drained.resolve(),
error(_s, e) {
ended.reject(e);
drained.reject(e);
closed.reject(e);
},
close: () => closed.resolve(),
},
});

const peerClosed = Promise.withResolvers<void>();
await Bun.connect({
hostname: "127.0.0.1",
port: server.port,
allowHalfOpen: true,
socket: {
open: s => s.shutdown(), // FIN
data() {},
end() {},
error: (_s, e) => peerClosed.reject(e),
close: () => peerClosed.resolve(),
},
});

const victim = await ended.promise;
await drained.promise;
// Going idle is not observable from JS: after drain returns, usockets drops
// the poll's events, and the event loop's next poll phase then consumes the
// FIN's final re-report. Two loop turns get past that poll phase; shutting
// down any earlier still finds an armed wakeup and passes without the fix.
for (let i = 0; i < 2; i++) await new Promise<void>(r => setImmediate(r));
victim.shutdown();
await closed.promise;
await peerClosed.promise;
});
});