Bun.serve response streaming and idempotency during client retries
0 reputation · 15 Jun 2024, 23:07 UTC
0 reputation · 15 Jun 2024, 23:07 UTC
When implementing high-throughput APIs using Bun.serve, the use of ReadableStream allows for efficient data transmission by avoiding full payload buffering. However, in scenarios where a client initiates a retry due to a perceived timeout or network flicker while a stream is already active, the server may continue processing the original request while simultaneously starting a new one.
Because Bun.serve operates on a high-performance native event loop, the timing of request cancellation and the subsequent cleanup of associated asynchronous resources must be precisely managed to prevent duplicate side effects, such as redundant database writes via bun:sqlite.
What is the documented mechanism for detecting a disconnected client during an active stream to halt further processing? How does Bun handle the lifecycle of the request handler when a retry occurs before the initial stream is fully closed?
31775 reputation · 16 Jun 2024, 06:39 UTC
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.
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.
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:
X-Request-ID header for every logical operation.X-Request-ID has already been processed.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);
}
});
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.
Use comments to ask for clarification. Post a solution as an answer.
31,775 reputation · 16 Jun 2024, 08:46 UTC
The existing answer correctly notes that write failures signal disconnects, but Bun.serve also exposes req.signal (an AbortSignal) that fires when the client connection closes. Handlers should wire this into stream production — for example, passing signal: req.signal to ReadableStream constructors or checking req.signal.aborted before each controller.enqueue() — to stop work before a failed write occurs.
Version note: req.signal behavior stabilized around Bun 1.1; earlier releases had inconsistent abort timing. Verify by logging req.signal.aborted in a long-running stream handler and aborting the client mid-flight.