fix(proxy): abort paid upstream on client disconnect for chat path - #279
Conversation
…0.12.254) The media handlers all abort their upstream on client disconnect (BlockRunAI#251, v0.12.252-253), but the main /v1/chat/completions path never did. proxyRequest wired its abort to req.on("close"), and on Node (verified on v24.15.0) an IncomingMessage emits "close" when the request-body readable finishes, not when the client hangs up. The body is fully drained near the top of proxyRequest, so that event had already passed by the time the listener was attached far below -- onClientClose never ran and a caller that hung up mid-request left the paid x402 upstream running to completion for a response nobody would receive. Move the abort to res.on("close") (guarded by !res.writableEnded), mirroring every media handler, and migrate the four removeListener cleanup sites to match. Covered by src/proxy.chat-abort.test.ts (upstream socket observes the abort after the client destroys its request).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (4)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe proxy now listens for response closure to abort unfinished paid chat upstream requests. Cleanup paths use the response listener. An end-to-end regression test verifies upstream closure after client disconnect. The package version and changelog were updated to v0.12.254. ChangesChat request cancellation
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The change localizes client-disconnect cancellation to the chat request path and adds regression coverage; no actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant Client
participant proxyRequest
participant UpstreamHTTPServer
Client->>proxyRequest: Send chat completion request
proxyRequest->>UpstreamHTTPServer: Forward paid request
Client--xproxyRequest: Close connection
proxyRequest->>UpstreamHTTPServer: Abort unfinished request
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (2 skipped: 2 unsupported.) ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…lient cancels as 300s timeouts (v0.12.254) Follow-ups on #279 (@erhnysr), landed in the same release: - proxyRequest now checks res.destroyed right after attaching the res "close" listener. Context compression and the balance check run between draining the body and the attach; a client that hung up in that window had already emitted "close", so the listener alone never fired and the paid upstream still ran. - A client cancel used to reach the shared abort exits and surface as "Request timed out after 300000ms" -> onError + balance cache invalidation. Introduce ClientDisconnectedError, return quietly from the catch, keep the cache. Regression test asserts onError is silent and gates the disconnect on the upstream being reached (no fixed 500ms timer).
|
Merged as-is via fast-forward ( |
Describe your changes
The media handlers all abort their upstream on client disconnect (#251, fixed for
/v1/images/image2imagein v0.12.252 and for audio + chat-path/img2imgin v0.12.253). The main/v1/chat/completionspath — the highest-traffic one — was the one handler still missing it.proxyRequestwired its disconnect abort toreq.on("close"):On Node (verified on v24.15.0) an
IncomingMessageemits"close"when the request-body readable finishes, not when the client hangs up. The body is fully drained near the top ofproxyRequest, so that event had already fired by the time the listener was attached ~1,500 lines below —onClientClosenever ran.globalControllerwas therefore only ever aborted by the request timeout, and a caller that hung up mid-request left the paid x402 upstream running to completion for a response nobody would receive — the exact orphan-upstream problem #251 fixed for the media paths.Change
Move the abort to
res.on("close")(guarded by!res.writableEnded), mirroring every media handler — e.g./v1/images/image2image:The four
removeListenercleanup sites are migrated fromreqtoresto match.Reproduction
New regression test
src/proxy.chat-abort.test.ts(same harness/style assrc/proxy.img2img-abort.test.ts): the client POSTs/v1/chat/completions, the upstream mock holds the socket open, and the clientdestroy()s mid-request. The mock'sres.on("close")-with-!writableEndedcounter observes the abort.upstreamAbortedstays0— the upstream is orphaned (surfaces asResponse finished with error: Premature close→All 1 models failed (HTTP 500)).upstreamAborted === 1), and the test also completes far faster since the abort fires immediately instead of waiting out the premature-close path.Full suite: 64 files / 724 tests passing.
Checklist
mainSummary by CodeRabbit
Bug Fixes
Tests
Chores