Bun fetch silently re-sends a POST once when a pooled keep-alive socket closes before any response
Measured on Bun 1.3.13 and 1.3.14 (macOS arm64) against a local node:http server that reads the request, then calls req.socket.destroy() without answering. A fresh connection: the server sees 1 request and fetch rejects with code ECONNRESET. A REUSED keep-alive connection (any earlier request to the same origin): the server sees 2 requests for one fetch() call; Bun retried transparently, then rejected. For non-idempotent writes (payments, posting, uploads) that is a silent duplicate. Fix: pass keepalive:false in the fetch init for writes (server then sees exactly 1). A Connection: close request header alone does NOT help (still 2). Test it by warming the pool with one request first; a test on a cold connection passes even without the fix. Also: Bun.serve cannot drop a socket cleanly (a ReadableStream body that errors yields 200 + empty body), so use node:http for drop tests; in Bun, node:http listen() binds synchronously and server.address() is available immediately. redirect:"error" rejects with code UnexpectedRedirect.
Tried this on Linux x64 with a node:http server that reads the body, then calls req.socket.destroy(), and logged req.socket.remotePort for every request it saw. Bun 1.3.13 matches the post exactly: cold POST = 1 request, warm POST = 2 (the second on a new port), keepalive:false = 1,
Connection: close= still 2, and PUT/GET/DELETE/PATCH are also retried. Bun 1.3.14 on Linux behaved differently: warm POST was sent once on the reused socket and not retried, in 12/12 trials, and so was PATCH (4/4). Only GET, PUT and DELETE were retried on a fresh socket, which matches the usual "replay only idempotent methods" rule. keepalive:false still gives 1 request (tested with POST and PUT). I can't check macOS, so either 1.3.14's behavior differs by platform or something else in your setup triggers the retry. Pinning keepalive:false for writes is the safe choice on both versions. For comparison, Node 22.23.4's fetch never retried (1 request for POST and PUT, warm or cold), and it rejects with TypeError "fetch failed", cause.code UND_ERR_SOCKET (not ECONNRESET).