fix: route authed urllib paths through a shared NoRedirectHandler opener (BE-3274)#530
fix: route authed urllib paths through a shared NoRedirectHandler opener (BE-3274)#530mattmillerai wants to merge 2 commits into
Conversation
…ner (BE-3274) Three authed call paths opened via plain urllib.request.urlopen, which follows 30x redirects with X-API-Key/Authorization still attached — bypassing the auth-leak guard NoRedirectHandler already provides for the rest of the codebase (comfy_client, cql/engine, cql/loader, transfer, cloud/oauth all open through a guarded opener). Consolidate request-building + opening into http.py behind build_authed_request/authed_urlopen over a shared _AUTHED_OPENER, and migrate the three unguarded paths: - command/workflow.py: _userdata_request + _http_request - command/models/search.py: _http_get_json - command/jobs.py: _cloud_cancel A refused redirect raises HTTPError, which every migrated call site already handles, so no caller changes are needed. Behavior is otherwise preserved: headers are not gated on is_cloud (local targets are simply uncredentialed), and byte caps / error mapping are untouched.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 35 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 2 finding(s).
| Severity | Count |
|---|---|
| 🟢 Low | 2 |
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
`build_opener()` implicitly installs FileHandler/FTPHandler/DataHandler
alongside the HTTP(S) ones. Every call site builds its URL from a trusted
`target.base_url`, so this isn't reachable today, but `_AUTHED_OPENER` is
the opener that attaches `X-API-Key` / `Authorization: Bearer`, so pin it
to http(s) and let anything else fall to UnknownHandler ("unknown url
type") rather than leaving a future caller one bad URL away from a
`file://` read. Proxy handling is unchanged (ProxyHandler still installed
when the env configures one).
Also drops the unrelated uv.lock churn that a `uv run` had swept into the
previous commit; main's lockfile is stale independently of this PR.
Raised by gemini-3.1-pro in the cursor-review panel.
|
Re-reviewed this PR after the CI went red. Both failures are pre-existing repo-wide breaks, not caused by this PR — the diff here touches only 1. It only shows up in CI because 2. Both are being tracked as separate follow-ups rather than fixed here, to keep this auth-hardening PR scoped. On this PR itself: both review threads are resolved (the http(s) opener pinning landed in 651aa76; the |
|
Correction to my previous comment: both CI breaks already have fixes in flight — I've confirmed neither needs a new ticket, and both are still unrelated to this PR.
So this PR just needs those to land (or a rebase once they do); nothing to do here. The substance of my previous comment stands — this PR's own diff is clean, both review threads are resolved, and the 132 affected tests pass locally. |
ELI-5
When our CLI calls the cloud with your API key attached and the server answers "go look over there instead" (a redirect), plain
urlopencheerfully follows — and hands your credential to wherever "over there" points. We already have a guard for exactly this (NoRedirectHandler), but three authed code paths were never wired up to it. This wires them up: a redirect on those paths now fails loudly instead of quietly replaying your key somewhere else.What changed
Adds a shared authed-request layer to
comfy_cli/http.pyand migrates the three unguarded paths onto it.comfy_cli/http.py— new, below the existingNoRedirectHandler:_AUTHED_OPENER = build_opener(NoRedirectHandler())— the shared no-redirect opener.build_authed_request(url, target, *, method, data, content_type)— attachesX-API-Key(preferred) orAuthorization: Bearer, plus optionalContent-Type.authed_urlopen(...)— builds the request and opens it through_AUTHED_OPENER.Migrated call sites (each drops its own private
_authed_request+urlopenpair):command/workflow.py—_userdata_request,_http_requestcommand/models/search.py—_http_get_jsoncommand/jobs.py—_cloud_cancelA refused redirect surfaces as
HTTPError, which every migrated site's existing error handling (_handle_cloud_http_error/_handle_local_http_error/_emit_http_error/ the_cloud_cancelHTTPErrorbranch) already covers — so there are no caller changes. Byte caps,_ResponseTooLarge, and error mapping are untouched.Headers are deliberately not gated on
target.is_cloud: the three sites don't gate today (local targets are simply uncredentialed), so this keeps the migration behavior-preserving apart from the intended redirect refusal.Why this is safe
NoRedirectHandlerand its "none of our authenticated endpoints redirect under normal operation" premise already ship onmainand are already applied to this same cloud API by six existing openers (comfy_client.py,cql/engine.py,cql/loader.py,command/transfer.py,cloud/oauth.py). This PR extends an existing, reviewed guard to three paths that were missed — it does not invent a new denial.install_openeris called nowhere in the codebase, so plainurlopenwas using the stock default opener.build_opener(NoRedirectHandler())yields the same default handler set (proxy handling included) with only the redirect handler swapped — the established pattern here._cloud_cancelpayload preserved: stillmethod="POST", data=b"", soContent-Length: 0is sent exactly as before.Verification
uv run --extra dev pytest).ruff format --checkclean (235 files).ruff checkshows 14 pre-existingUP038errors, byte-identical to theorigin/mainbaseline (verified by diffing the lint output against a stashed tree) — none in touched files.Acceptance criteria
git grep 'add_header("X-API-Key"' comfy_cli/no longer matchesworkflow.py,models/search.py, orjobs.py. Remaining matches arecomfy_client.py+cql/engine.py(explicitly out of scope — already guarded) andhttp.py(the new shared helper).urllib.request.urlopenremains on an auth-attaching path. The survivors —jobs.pylocal queue/interrupt andtemplates.py— were each grep-verified to attach no auth headers.comfy_client.py,cql/engine.py,cql/loader.py,transfer.py,cloud/oauth.pyuntouched.Tests added (
tests/comfy_cli/test_http.py)X-API-Keyprecedence when both credentials are set; Bearer-only; no auth header when uncredentialed;Content-Typepassthrough (and absence by default);method/datapassthrough;authed_urlopenopens via_AUTHED_OPENERwith the right Request/timeout; a refused 302 propagates asHTTPError; and an end-to-end check that a migrated caller (search's_http_get_json) propagates a refused redirect.Existing mocks that patched
urllib.request.urlopenand would now miss the new code path were repointed atcomfy_cli.http._AUTHED_OPENER.open(test_search.py,test_workflow_saved.py).test_jobs.py's_capture_urlopenpatches both — its local queue/interrupt tests still exercise plainurlopen, while cloud cancel now goes through the authed opener.Judgment calls
workflow.pyintentionally keeps urllib out of its top-level imports, soauthed_urlopenis imported lazily inside the functions there, preserving that style.search.pyandjobs.pyalready import urllib at module top level, so the helper is imported at top level there to match each module's existing convention. (search.py's now-unusedimport urllib.requestwas removed;urllib.error/urllib.parseare still used.)install.py,models/models.py, andgenerate/client.pyattachAuthorization: Bearerbut use a different HTTP stack (not urllib), so they are outside this urllib-focused pass. Consolidating the six existing guarded openers onto_AUTHED_OPENERis likewise a separate follow-up.NoRedirectHandlerand is already relied upon against the same API by the six openers listed above. Flagging explicitly so a reviewer with cloud access can veto if any of/userdata,/api/experiment/models,/api/assets, or/api/jobs/<id>/cancelis expected to 30x in normal operation.