Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(request): stream-stall failover, private-header prefix block, pinned-index message - #546

Closed
ndycode wants to merge 2 commits into
claude/audit-15-rotation-proxy-carvefrom
claude/audit-30-stream-stall-fix
Closed

ndycode wants to merge 2 commits into
claude/audit-15-rotation-proxy-carvefrom
claude/audit-30-stream-stall-fix

Conversation

@ndycode

@ndycode ndycode commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes three latent issues in the modules #532 extracted — all pre-existing behavior moved verbatim by the carve (same code on main inside runtime-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.

Stacked on #532 (claude/audit-15-rotation-proxy-carve). Merge #532 first; this PR then shows only the two fix commits.

Fix 1 — stream stall forwarded as clean success (withTimeout ordering)

withTimeout invoked onTimeout before rejecting. forwardStreamingResponse's onTimeout cancels the stream reader, and cancelling a reader 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. Fix: reject first, then run onTimeout. readErrorBody (the only other caller) passes a no-op onTimeout and is unaffected.

Fix 2 — private account headers blocked by prefix (CodeRabbit, security guideline)

responseHeadersForClient filtered 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 entire x-codex-multi-auth-account- prefix.

Fix 3 — null pinned index rendered as "Pinned account 1" (CodeRabbit)

On the pin-desync path buildPinnedUnavailableErrorBody reported pinnedAccountIndex: null but 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=0
  • npx vitest run rate-limit-decision + stream-failover-runtime + issue-474-pin-honored + runtime-rotation-proxy: 147 passed, 2 failed — the 2 are the known IPv6 ::1 bind environment failures from the documented baseline

Risk / 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 withTimeout where onTimeout was called before reject, allowing reader.cancel() to settle the pending read() with {done: true} ahead of the rejection — causing Promise.race to 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.

  • withTimeout ordering fix: reject() now fires before onTimeout(), ensuring the race rejection is enqueued first; stalled streams now correctly destroy the response, set lastError, and fire onStreamError
  • prefix-based header filter: PRIVATE_CLIENT_RESPONSE_HEADERS set replaced with startsWith(\"x-codex-multi-auth-account-\") — any future account-identifying header is blocked by default instead of leaking until added to an allowlist
  • null-index message fix: buildPinnedUnavailableErrorBody no longer fabricates "Pinned account 1" when pinnedIndex is null, keeping the human-readable message consistent with pinnedAccountIndex: null

Confidence 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

Filename Overview
lib/request/stream-failover-runtime.ts core fix: swaps reject/onTimeout order in withTimeout so race rejection is enqueued before reader.cancel() can settle the read promise; also upgrades private header filter from exact Set to prefix match
test/stream-failover-runtime.test.ts stall test flipped from bug-pinning to correct expectations; new prefix regression test added; withTimeout ordering not directly unit-tested at the function boundary
lib/request/rate-limit-decision.ts null-index desync path now emits "The pinned account" instead of fabricating "Pinned account 1", keeping human-readable message consistent with machine-readable pinnedAccountIndex: null
test/rate-limit-decision.test.ts test description and assertion updated to match fixed null-index message; regression note added
test/issue-474-pin-honored.test.ts integration assertion updated to expect "The pinned account" and explicitly rejects "Pinned account 1" on the desync path

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 fires
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
test/stream-failover-runtime.test.ts:135-152
**missing ordering-specific unit test for `withTimeout`**

the existing `withTimeout` stall test covers the rejection path but passes a promise that can never settle on its own (`new Promise<never>(() => undefined)`), so it doesn't exercise the exact failure mode this PR fixes — where `onTimeout`'s side effects *do* settle `promise` before the rejection wins the race. the regression is proven end-to-end via the `forwardStreamingResponse` stall test, but a targeted `withTimeout` unit test (one where `onTimeout` triggers settlement of the inner promise, e.g. by cancelling a real stream reader) would pin the ordering guarantee closer to the function itself and survive a refactor of `forwardStreamingResponse`.

Reviews (2): Last reviewed commit: "fix(request): block private account head..." | Re-trigger Greptile

…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
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ndycode, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: dfd444f5-8066-4350-9b5d-c63e0ce295ec

📥 Commits

Reviewing files that changed from the base of the PR and between 4d5dd51 and 3d08e35.

📒 Files selected for processing (5)
  • lib/request/rate-limit-decision.ts
  • lib/request/stream-failover-runtime.ts
  • test/issue-474-pin-honored.test.ts
  • test/rate-limit-decision.test.ts
  • test/stream-failover-runtime.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/audit-30-stream-stall-fix
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-30-stream-stall-fix

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ndycode pushed a commit that referenced this pull request Jun 10, 2026
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
@ndycode ndycode changed the title fix(request): reject before onTimeout so stream stalls fail instead of ending cleanly fix(request): stream-stall failover, private-header prefix block, pinned-index message Jun 10, 2026
ndycode pushed a commit that referenced this pull request Jun 10, 2026
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
ndycode added a commit that referenced this pull request Jun 10, 2026
fix(request): stream-stall failover, private-header prefix block, pinned-index message
@ndycode ndycode closed this Jun 10, 2026
ndycode pushed a commit that referenced this pull request Jun 10, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants