Repository navigation
fix(proxy): check x-headroom-base-url off the event loop in chat and responses - #4856
chopratejas wants to merge 8 commits into
Conversation
…responses The chat-completions and responses handlers validated the client-chosen upstream with the blocking guard, which waits up to the resolve timeout on the event-loop thread, and did so up to three times per request. A slow resolving host therefore held up every other in-flight request. Both handlers now resolve the header once per request through the async guard, which runs the lookup on a worker thread, and reuse that result for the destination and the extra-header decision. Assay-Kind: bugfix Assay-Ask: The chat and responses handlers wait on DNS for x-headroom-base-url on the event-loop thread, stalling other requests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR governanceThis PR does not yet satisfy the required template fields:
Please update the PR body, or move the PR back to draft while it is still in progress. |
… in chat The chat handler now checks the override once, before image compression. The guard's pin lasts 60 s, so a compression that ran longer left only an expired pin and the connection was refused. After image compression runs, the same URL is checked again to renew the pin; the destination is not chosen again, and a host that no longer passes is refused with 400. Assay-Kind: bugfix Assay-Ask: A chat request with a custom base URL is refused at connect time when image compression outlasts the upstream pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…y tests Two chat tests replaced the synchronous guard to let deliberately unresolvable hosts through. The chat handler now awaits the async guard, so the stub no longer applied and the override was dropped. Stub the function the handler calls. Assay-Kind: tests Assay-Ask: Two chat cache-stability tests stub the sync guard, which the chat handler no longer calls, so their custom hosts are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assay-Kind: deps Assay-Ask: The dependency audit checks failed on this PR because the branch predates the dependency updates on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JerrettDavis
left a comment
There was a problem hiding this comment.
Review completed; approval remains pending the items below.
Reviewed head d4eb81585181e5ff5b86f1f7fc0534a5513fb588 against main 0368a872cdd5064b39888e13847200ff562ac192.
Qualification still required:
- Passing test evidence is missing
- Exact-source provenance review is incomplete
… loop The post-image-compression check of x-headroom-base-url ran on every image request, even with a still-valid pin, so a failed second lookup could refuse a valid request; on one branch it also waited for DNS on the event loop. The check now runs only when this request's pin for that host has expired, and always through the async guard. Assay-Kind: bugfix Assay-Ask: The chat handler re-resolves the custom base URL after every image compression, on the event loop and even when the pin is still valid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JerrettDavis
left a comment
There was a problem hiding this comment.
Review completed; approval remains pending the items below.
Reviewed head 1416d0d8c5276d1b84c7f09e1facaae97450a714 against main 0368a872cdd5064b39888e13847200ff562ac192.
Qualification still required:
- Hosted check evidence changed
- Exact-source provenance review is incomplete
- Check/run 132232314322 is not terminal
- Check/run 132232314308 is not terminal
- Check/run 132232314254 is not terminal
- Check/run 132232314208 is not terminal
- Check/run 43525780933 is not terminal
JerrettDavis
left a comment
There was a problem hiding this comment.
Reviewed 1416d0d against target 0368a87. The async guard preserves request-scoped pins and routing/header decisions; both image-compression pin findings are resolved. Local validation passed 17 handler/routing cases and 26 cache integration cases (the latter in documented Python-only mode because the worker lacks the native extension). Exact-head hosted CI, including all four test shards, is now successful. No blocking code issues found.
Sync PR #4856 with latest main.
JerrettDavis
left a comment
There was a problem hiding this comment.
Reviewed 55828ed against target b324efb. The async guard preserves routing, header trust and request-scoped pins; both prior image-compression pin findings remain resolved. Fresh local validation passed 27 handler/routing cases and 26 ASGI cache cases in documented Python-only mode. The new off-loop regression fails on the recorded base for both handlers and passes on head. Staged exact-head CI is successful, including all four test shards. No blocking code issues found. Native compression and real DNS/provider traffic were not tested locally.
Conflict-only merge. Kept this branch's async resolver for the chat and responses handlers and main's lapsed-pin re-check after image compression (which already uses the async guard); took main's resolve-once test. Assay-Kind: config Assay-Ask: The branch conflicts with main after the sibling upstream fix was merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o fix/upstream-guard-off-loop Brings in the earlier merge of main already on the branch; no content change. Assay-Kind: config Assay-Ask: The branch conflicts with main after the sibling upstream fix was merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Jerrett: conflict-only merge with main (239f0de, plus 3582001 to take in your earlier merge commit; no content change from that one). Kept this PR's async resolver in the chat and responses handlers, and main's lapsed-pin re-check after image compression, which already goes through the async guard. Took main's resolve-once test. Area tests, ruff and mypy pass locally. Your approval was dismissed by the push, so I re-requested review. |
|
The one red check, test (3), is |
Description
handle_openai_chatandhandle_openai_responsesvalidatedx-headroom-base-urlwith the synchronousis_safe_upstream_url, which waits for the DNS lookup (up toHEADROOM_UPSTREAM_RESOLVE_TIMEOUT_S, 3 s by default) on the event-loop thread. Chat did this three times per request and responses twice, so one slow-resolving host held up every other in-flight request for several seconds. The other routes inheadroom/providers/proxy_routes.pyalready useis_safe_upstream_url_async.Both handlers now resolve the header once, through a new
_resolve_openai_upstream_base_asyncthat uses the async guard (lookup on a worker thread, pin scope opened on the request's task), and reuse that result for the destination and the extra-header decision.Type of Change
Changes Made
headroom/proxy/handlers/openai.py: header parsing split into_openai_base_url_candidate; sync_resolve_openai_upstream_base(kept for_resolve_openai_upstream) and new_resolve_openai_upstream_base_asyncshare it;_openai_upstream_from(request, custom)picks the upstream from an already-resolved override; chat and responses call the async resolver once.tests/test_proxy/test_openai_upstream_guard_off_loop.py: asserts the guard runs once and not on the event-loop thread, for chat and responses.tests/test_proxy_openai_cache_key_integration.py: its stub now patches the async resolver the handler calls.Sibling PRs that touch the same code (merge order note):
fix/upstream-idna-pin-onceadds the identical_openai_upstream_fromand resolve-once hunks (this PR is a superset of that part);fix/openai-base-url-rejectchanges the rejection branch of_resolve_openai_upstream_base, which after this PR must be mirrored in_resolve_openai_upstream_base_async. Suggested order: idna-pin-once, then this PR, then reject (rebasing reject means raising the 400 from the shared rejection path in both resolvers).Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
create_appdriven throughhttpx.ASGITransporton one event loop;socket.getaddrinforeplaced soslow.exampletakes 2 s to answer (everything else real)./v1/chat/completionsrequest withx-headroom-base-url: https://slow.example/v1, then 0.2 s later sendGET /livezand time it.Runtime Rollout Safety
Review Readiness
Checklist
CHANGELOG.md— it is generated by release-please from my Conventional Commit PR title (a CI guard enforces this)Additional Notes
Fix adapted from a proposal by HetCreep.
Follow-up, not in this PR:
select_passthrough_base_url(headroom/providers/proxy_targets.py, Azureapi-keybranch) andheadroom/providers/registry.pystill call the sync guard; the passthrough route already checks the same header with the async guard first, so the sync call there is a second lookup on the loop.🤖 Generated with Claude Code
Reported by @JoaoMarcos44. Thank you for your contribution! 🩵