Conversation
…f ending cleanly
withTimeout invoked onTimeout before rejecting. forwardStreamingResponse's
onTimeout cancels the stream reader, and cancelling settles the pending
read() with {done: true} ahead of the rejection in the microtask queue, so
Promise.race resolved: a stalled upstream stream was forwarded to the
client as a clean end-of-stream — truncated body with a normal end(), no
status.lastError, and the onStreamError failover hook never fired. The
stall branch of the catch block was unreachable.
Rejecting first enqueues the race's rejection ahead of any settlement the
onTimeout side effects can cause, restoring the intended stall handling:
the response is destroyed, lastError records the stall, and onStreamError
fires. readErrorBody (the only other caller) uses a no-op onTimeout and is
unaffected.
The regression test pinning the old behavior is flipped to the intended
expectations and documents the mechanism.
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. |
|
Warning Review limit reached
More reviews will be available in 4 minutes and 26 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
✨ Finishing Touches🧪 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 |
Adds the follow-up table (#543-#546 plus the per-branch unit suites) and updates the remaining-deferred note now that proxy phase 2 and login phase 4 are in progress. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
…ating a pinned index Two review findings on the extracted modules, both pre-existing behavior moved verbatim by the phase-1 carve, fixed here to keep that PR zero-behavior-change: - responseHeadersForClient filtered account metadata by an exact-name allowlist, so a future x-codex-multi-auth-account-* header would leak to clients by default. The filter now blocks the whole prefix. - buildPinnedUnavailableErrorBody rendered a null pinned index (the desync path) as "Pinned account 1" while the machine-readable pinnedAccountIndex stayed null. The message now says "The pinned account" when the index is unknown. Tests updated/added for both, including the pre-existing null-index pin in test/issue-474-pin-honored.test.ts. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
The quota-refresh write races and the small-suite mock-factory migration are delivered; remaining deferred work narrows to the giant-suite migrations only. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
fix(request): stream-stall failover, private-header prefix block, pinned-index message
Inline the #544 isRecord and #546 stream-stall references so the 'surfaced real bugs twice' claim is verifiable without scrolling to section 5.1. https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Summary
Fixes three latent issues in the modules #532 extracted — all pre-existing behavior moved verbatim by the carve (same code on
maininsideruntime-rotation-proxy.ts), fixed here so #532 stays zero-behavior-change. The first was found by writing #532's unit suites; the other two are CodeRabbit review findings on #532.Fix 1 — stream stall forwarded as clean success (
withTimeoutordering)withTimeoutinvokedonTimeoutbefore rejecting.forwardStreamingResponse'sonTimeoutcancels the stream reader, and cancelling a reader settles the pendingread()with{done: true}ahead of the rejection in the microtask queue — soPromise.raceresolved. A stalled upstream stream was forwarded to the client as a clean end-of-stream: truncated body with a normalend(), nostatus.lastError, and theonStreamErrorfailover hook never fired; the stall branch of the catch block was unreachable. Fix: reject first, then runonTimeout.readErrorBody(the only other caller) passes a no-oponTimeoutand is unaffected.Fix 2 — private account headers blocked by prefix (CodeRabbit, security guideline)
responseHeadersForClientfiltered account metadata with an exact-name allowlist (x-codex-multi-auth-account-{index,label,email,id}); a future header under the same prefix would have leaked to clients by default. The filter now blocks the entirex-codex-multi-auth-account-prefix.Fix 3 — null pinned index rendered as "Pinned account 1" (CodeRabbit)
On the pin-desync path
buildPinnedUnavailableErrorBodyreportedpinnedAccountIndex: nullbut the human-readable message claimed "Pinned account 1", contradicting the payload. The message now says "The pinned account is currently unavailable…" when the index is unknown; the machine-readable fields are unchanged.Changes
lib/request/stream-failover-runtime.ts: timer-callback ordering swap (with the ordering constraint documented); prefix-based private-header filter replacing the exact-name set.lib/request/rate-limit-decision.ts: null-index message branch.test/stream-failover-runtime.test.ts: stall test flipped from pinned-bug to intended behavior; new prefix-coverage case (x-codex-multi-auth-account-plan/ mixed-case future field blocked).test/rate-limit-decision.test.ts+test/issue-474-pin-honored.test.ts: null-index expectations updated to the corrected message (the issue-474 test previously pinned the contradictory message).Validation
npm run typecheck; eslint on all touched files--max-warnings=0npx vitest runrate-limit-decision + stream-failover-runtime + issue-474-pin-honored + runtime-rotation-proxy: 147 passed, 2 failed — the 2 are the known IPv6::1bind environment failures from the documented baselineRisk / Rollback
Three small, isolated fixes; the only client-visible deltas are that stalled streams now take the (previously dead) error path, unknown future account headers are stripped, and the desync message no longer fabricates an index. Revert the two commits to roll back.
https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
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
fixes a latent race condition in
withTimeoutwhereonTimeoutwas called beforereject, allowingreader.cancel()to settle the pendingread()with{done: true}ahead of the rejection — causingPromise.raceto resolve a stalled stream as a clean end-of-stream. also fixes a null-index desync message bug and upgrades the private header filter from an exact-name set to a prefix check.withTimeoutordering fix:reject()now fires beforeonTimeout(), ensuring the race rejection is enqueued first; stalled streams now correctly destroy the response, setlastError, and fireonStreamErrorPRIVATE_CLIENT_RESPONSE_HEADERSset replaced withstartsWith(\"x-codex-multi-auth-account-\")— any future account-identifying header is blocked by default instead of leaking until added to an allowlistbuildPinnedUnavailableErrorBodyno longer fabricates "Pinned account 1" whenpinnedIndexis null, keeping the human-readable message consistent withpinnedAccountIndex: nullConfidence Score: 5/5
safe to merge — the change is a two-line swap in a single timer callback, isolated to withTimeout, and the stall error path it restores was already fully implemented; the only behavioral delta is that stalled streams now take the intended error path
the ordering fix is mechanically correct: reject() enqueues the rejection synchronously before any reader.cancel() microtask can settle the read() promise, so Promise.race cannot resolve ahead of it. the forwardStreamingResponse stall test proves the end-to-end behavior and is a reliable regression pin. the two companion fixes (null-index message, prefix header filter) are small and independently verified by their own test updates.
test/stream-failover-runtime.test.ts — the withTimeout ordering guarantee is only covered end-to-end via the forwardStreamingResponse stall test; a direct withTimeout unit test that lets onTimeout settle the inner promise would make the regression harder to miss in a future refactor
Important Files Changed
Sequence Diagram
sequenceDiagram participant Timer as setTimeout callback participant Race as Promise.race participant Read as reader.read() participant Cancel as reader.cancel() note over Timer,Cancel: BEFORE fix (buggy) Timer->>Cancel: onTimeout() → reader.cancel() Cancel-->>Read: "settles {done:true} (microtask)" Timer->>Race: reject(Error) Read-->>Race: "resolves {done:true} wins race ❌" note over Race: resolves — stall silently ends stream note over Timer,Cancel: AFTER fix (correct) Timer->>Race: reject(Error) note over Race: rejection enqueued in microtask queue Timer->>Cancel: onTimeout() → reader.cancel() Cancel-->>Read: "settles {done:true} (later microtask)" Race-->>Race: rejection wins — caught by forwardStreamingResponse ✅ note over Race: rejects — lastError set, res.destroy(), onStreamError firesPrompt To Fix All With AI
Reviews (2): Last reviewed commit: "fix(request): block private account head..." | Re-trigger Greptile