fix(request): respect client backpressure when forwarding streams - #551
Conversation
forwardStreamingResponse ignored res.write()'s return value, so a slow client buffered the whole upstream stream in process memory. When a write reports a full socket buffer, the forwarder now pauses upstream reads until drain; the waiter also settles on close/error so a client that disconnects mid-backpressure cannot park the forwarder — the next read observes the cancellation installed by the close handler. Tests pin the write ordering around drain (the source's pull order is not asserted: ReadableStream prefetches into its internal queue independently of the forwarder's pacing) and the disconnect-during- backpressure path. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughstream forwarder now handles client backpressure by awaiting socket drain when ChangesStream backpressure flow control
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
review notes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
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 |
Review follow-ups on the backpressure fix: the fake response now throws on write-after-destroy like a real ServerResponse (the old fake silently accepted writes, leaving the destroyed-stream path unreachable), a new test pins that a socket error during backpressure surfaces through the catch block (waitForDrain settles silently; the next write throws and records lastError + fires onStreamError), and a three-chunk case pins one drain wait per backpressured write. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Summary
Implements the backpressure handling CodeRabbit suggested on #532's review (now merged):
forwardStreamingResponseignoredres.write()'s return value, so a slow client caused the whole upstream stream to buffer in process memory instead of pacing the upstream reads.Changes
lib/request/stream-failover-runtime.ts: when a write reports a full socket buffer, the forwarder now waits fordrainbefore the next upstream read. The waiter also settles onclose/error, so a client that disconnects mid-backpressure cannot park the forwarder forever — the nextreader.read()then observes the cancellation installed by the existing close handler.test/stream-failover-runtime.test.ts: two new deterministic tests — write ordering arounddrain(deliberately asserting write order, not the source's pull order: ReadableStream prefetches into its internal queue independently of the forwarder's pacing), and the disconnect-during-backpressure path.Validation
npm run typecheck; eslint on both files--max-warnings=0npx vitest run test/stream-failover-runtime.test.ts test/runtime-rotation-proxy.test.ts— 92 passed, 2 failed: the two known IPv6::1bind environment failures from the documented baselinemain(e453111) into the branchRisk / Rollback
The only behavioral delta is pacing: chunks and termination semantics are unchanged; fast clients never hit the wait. Revert the single fix commit.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Generated by Claude Code
note: greptile review for oc-chatgpt-multi-auth. cite files like
lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.Greptile Summary
implements backpressure handling in
forwardStreamingResponseby checkingres.write()'s return value and awaiting a newwaitForDrainhelper before the next upstream read; also adds a fourth test covering the error-during-backpressure path that was flagged on the previous review.waitForDrainsettles ondrain,close, orerror, preventing a disconnecting client from parking the forwarder forever; the error path surfaces through the subsequentres.write()throw, which the existing catch block records correctly.streamStallTimeoutMsguards onlyreader.read(), so a slow client that holds the connection open without draining can park the forwarder and its upstream account slot indefinitely.Confidence Score: 4/5
safe to merge; the core backpressure logic is correct and all three settlement paths are tested, but the drain wait carries no application-level ceiling
the forwarder correctly pauses upstream reads and resumes on drain/close/error, and the new error-during-backpressure test fills the gap flagged in the previous review. the one open concern is that waitForDrain has no timeout of its own — streamStallTimeoutMs only guards reader.read(), so a client that holds the TCP connection open without draining or closing will park the forwarder and the upstream API account slot for however long OS-level keepalives take to fire
lib/request/stream-failover-runtime.ts — specifically the unbounded drain wait in waitForDrain
Important Files Changed
Sequence Diagram
sequenceDiagram participant U as Upstream API participant F as forwardStreamingResponse participant W as waitForDrain participant C as Client Socket F->>U: reader.read() guarded by streamStallTimeoutMs U-->>F: chunk F->>C: res.write(chunk) alt write returns true C-->>F: ok, continue else write returns false - backpressure F->>W: await waitForDrain(res) W->>C: once drain or close or error alt client drains C-->>W: drain event W-->>F: resolve F->>U: reader.read() next chunk else client disconnects C-->>W: close event W-->>F: resolve note over F: close handler cancelled reader, next read returns done else socket error C-->>W: error event W-->>F: resolve F->>C: res.write() throws ERR_STREAM_DESTROYED note over F: catch block records error and calls onStreamError end endPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "test: cover error-during-backpressure an..." | Re-trigger Greptile