Handling Client Disconnections in Bun.serve
Bun does not provide a specific "onDisconnect" event callback within the Bun.serve configuration. Instead, the mechanism for detecting a disconnected client during an active stream is the termination of the ReadableStream. When a client closes the connection or a network flicker occurs, the underlying socket is closed, and any attempt by the server to push more data into the stream's controller will throw an error or result in a rejected promise.
Lifecycle of Retries and Request Handlers
Bun treats every incoming HTTP request as a discrete event. If a client initiates a retry before the initial stream is fully closed, Bun spawns a completely new request handler. The original handler continues to execute until it attempts to write to the now-closed socket or completes its internal asynchronous logic. Because these handlers run independently on the event loop, they can overlap, leading to duplicate side effects (such as redundant bun:sqlite writes) if the logic is not idempotent.
Preventing Duplicate Side Effects
To ensure idempotency during streaming retries, you must implement a correlation mechanism. Since Bun does not automatically link a retry to a previous request, follow these steps:
- Client-Side Request IDs: Require the client to send a unique
X-Request-ID header for every logical operation.
- Server-Side State Tracking: Before performing a state-changing operation (e.g., a database write), check if the
X-Request-ID has already been processed.
- Graceful Stream Termination: Wrap your stream's
read or push logic in a try...catch block. When a write fails due to a disconnected client, immediately cancel any pending asynchronous tasks associated with that specific request.
Bun.serve({
async fetch(req) {
const requestId = req.headers.get("X-Request-ID");
// 1. Verify idempotency key in DB here
const stream = new ReadableStream({
async start(controller) {
try {
while (true) {
const data = await fetchData();
controller.enqueue(new TextEncoder().encode(data));
}
} catch (e) {
console.error("Client disconnected, halting processing");
// 2. Cleanup resources/cancel DB transactions here
} finally {
controller.close();
}
}
});
return new Response(stream);
}
});
Verification and Assumptions
This behavior is based on the standard implementation of the Web Streams API within the Bun runtime. To verify this in your environment, implement a handler that increments a global counter on every request and manually terminate the connection using curl mid-stream; you will observe the counter incrementing again upon retry despite the first request's logic potentially still being active in the event loop.
Missing Diagnostic Detail: Are you using a reverse proxy (like Nginx or Caddy) in front of Bun? Proxies often handle timeouts and retries differently, which may mask or accelerate the socket closure signal sent to Bun.