Skip to content

fix: type registry API failures and surface machine-readable error codes (BE-3271) - #528

Merged
mattmillerai merged 9 commits into
mainfrom
matt/be-3271-registry-api-typed-errors
Aug 10, 2026
Merged

fix: type registry API failures and surface machine-readable error codes (BE-3271)#528
mattmillerai merged 9 commits into
mainfrom
matt/be-3271-registry-api-typed-errors

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

ELI-5

When you comfy node publish or comfy node registry-install and the registry
says "no" (bad token, node not found, server error), the CLI used to blow up
with a raw Exception — a generic message or a Python traceback with no
machine-readable error code
. An agent driving the CLI couldn't tell what
went wrong programmatically.

This PR gives those failures a name and a code: the registry client now raises a
typed RegistryAPIError that carries the HTTP status and body, and the
publish/install commands turn that into the standard
renderer.error(code=..., details={status, body}) envelope — the same
structured-error contract the rest of the CLI already uses.

What changed

  • comfy_cli/registry/api.py — new RegistryAPIError(Exception) carrying
    optional status / body. The 5 bare raise Exception(...) (publisher-id
    / project-name validation, and the publish / list / install HTTP-failure
    branches) now raise RegistryAPIError. Any existing broad except Exception
    keeps working — RegistryAPIError subclasses Exception.
  • Hardening (55d620c) — response bodies are untrusted input, so
    sanitize_error_body() redacts the publish PAT (the publish request body
    carries it, and the registry can echo it back), escapes CR/LF so a body can't
    forge extra log lines, and bounds the body to 2000 chars. This means the
    message text is no longer byte-for-byte identical to the old
    raise Exception(...) for HTTP-failure branches — bodies may be redacted,
    escaped, or truncated. The two client-side validation messages are unchanged.
  • comfy_cli/command/custom_nodes/command.py — the publish and
    registry-install handlers gain a typed except RegistryAPIError (before the
    existing broad except) that emits renderer.error(code="node_publish_failed" | "node_install_failed", details={status, body}) and exits 1.
  • comfy_cli/error_codes.py — registers the two new codes with navigation
    hints (the registry test enforces every raised code is registered and every
    registered code carries a hint).
  • Teststest_api.py now asserts the typed exception + that status/body
    are carried (plus a client-side-validation case with status=None); new
    command-boundary tests assert publish/install surface the right code + details
    and exit 1.

Scope / judgment calls (this is the narrowed BE-3271, a DOWNGRADE from a

broader idiom sweep)

  • Deliberately scoped to publish/install boundaries, per the ticket wording.
    The list_all_nodes API call is converted to the typed exception (1 of the 5),
    but its hidden registry-list command boundary (display_all_nodes) is left
    untouched
    — it still catches RegistryAPIError as Exception, logs, shows
    the friendly message, and returns exit 0, exactly as before. Zero behavioral
    change there.
  • The wider typer.echo / ui.display_error_message migrations across
    custom_nodes/models were intentionally NOT done
    (the ticket explicitly defers
    those until those commands join the JSON-envelope contract).
  • One intentional behavioral change: on a registry API failure (non-2xx),
    registry-install now exits 1 instead of silently returning 0. This is
    a correctness fix — the sibling download-path test already documents the intent
    ("Must exit non-zero so automation / CI can detect the failure") — and it is
    narrowly scoped: only RegistryAPIError takes the new path; connection errors
    and everything else still hit the unchanged broad except (exit 0).
  • Fixed in passing (55d620c): the ui.display_error_message({str(e)})
    set-literal quirk in the publish broad-except fallback — it wrapped the
    message in a one-element set. Now passes str(e). One-character fix flagged
    in review; taken rather than deferred.
  • Deferred: making registry-install exit non-zero for non-RegistryAPIError
    failures (connection error, timeout, JSON decode) is tracked as
    BE-3294 / PR fix(registry): exit non-zero on non-RegistryAPIError registry-install failures #538.

Verification

  • ruff format --check: clean. ruff check: no errors in any file this PR touches.
  • This PR's own tests: 101 passed (tests/comfy_cli/registry/test_api.py +
    tests/comfy_cli/command/nodes/).

CI red is external to this PR (verified, not assumed)

Both go green on a rebase once those land.

@mattmillerai mattmillerai added the agent-coded PR authored by the agent-work loop label Jul 17, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 17, 2026 05:44
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cd1ec101-aff1-4152-a2ed-a29599f8eed5

📥 Commits

Reviewing files that changed from the base of the PR and between 87f0414 and 12f7fba.

📒 Files selected for processing (7)
  • comfy_cli/command/custom_nodes/command.py
  • comfy_cli/error_codes.py
  • comfy_cli/registry/__init__.py
  • comfy_cli/registry/api.py
  • tests/comfy_cli/command/nodes/test_node_install.py
  • tests/comfy_cli/command/nodes/test_publish.py
  • tests/comfy_cli/registry/test_api.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3271-registry-api-typed-errors
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3271-registry-api-typed-errors

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

@mattmillerai mattmillerai added the cursor-review Request Cursor bot review label Jul 17, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 4 finding(s).

Severity Count
🟡 Medium 2
🟢 Low 1
⚪ Nit 1

Panel: 6/8 reviewers contributed findings.

Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)

Comment thread comfy_cli/command/custom_nodes/command.py
Comment thread comfy_cli/command/custom_nodes/command.py
Comment thread comfy_cli/command/custom_nodes/command.py
Comment thread comfy_cli/command/custom_nodes/command.py Outdated
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 17, 2026
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 Reviews-loop pass: no code changes needed — both red checks are external to this PR.

Review threads: all 4 Cursor findings were already addressed in 55d620c (PAT redaction + body bounding at the raise sites, and the {str(e)} set-literal nit). The one deferral is tracked as BE-3294 and is now PR #538. Nothing outstanding. CodeRabbit never reviewed — it is rate-limited, not blocking.

build (9 failures) — upstream dep regression, not this PR. This PR does not touch config_parser.py; the failures are ValueError: Comment cannot contain line breaks from tomlkit 0.15.1, released today 01:48 UTC, ~5h before the 07:08 run. Bisected locally:

  • tomlkit==0.15.1 → 8 failed in test_config_parser.py + test_node_init_strips_credentials (= all 9 CI failures)
  • tomlkit==0.13.3 (what uv.lock pins) → 81 passed

build resolves deps fresh instead of from the lock, so it picked up 0.15.1. Same red is on unrelated PRs #534/#535. Already being fixed by #533/#536 (config_parser) and #537 (install from uv.lock) — deliberately not duplicating that work here to avoid conflicting with those PRs.

test (Windows) — also external: ImportError: cannot import name __version__ from pydantic_core during dep install, via mixpanel. Covered by #529/#535.

This PR on its own is green: 101/101 tests in test_api.py + tests/comfy_cli/command/nodes/ pass, ruff format --check clean, and no ruff errors in any file this PR touches. It should go green on a rebase once the tomlkit fix lands on main.

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 Reviews-loop re-check: no code changes needed — this PR is ready; both red checks are still external.

Independently re-verified rather than trusting the earlier pass:

Also refreshed the PR description, which had gone stale after 55d620c: it still claimed error messages were "byte-for-byte equivalent" (no longer true — bodies are now redacted/escaped/truncated, which reviewers should know) and listed the set-literal quirk as "left alone" when it was actually fixed.

Goes green on a rebase once the tomlkit fix lands on main. Not duplicating that work here to avoid conflicting with those PRs.

mattmillerai and others added 2 commits July 22, 2026 12:40
…des (BE-3271)

Replace the 5 bare `raise Exception(...)` in comfy_cli/registry/api.py with a
typed RegistryAPIError carrying the HTTP status/body, and map it at the
publish/install command boundary to renderer.error(code=..., details={status,
body}) so node publish/install failures surface a machine-readable code instead
of a raw traceback or generic message. Messages are kept equivalent.
…rors (BE-3271)

Addresses the cursor-review panel findings on the new
renderer.error(details={status, body}) surface:

- Add sanitize_error_body(): redacts known secrets, escapes CR/LF, and
  truncates to MAX_ERROR_BODY_CHARS before the untrusted registry
  response body reaches a log record or an error envelope.
- publish_node_version() passes the PAT as a secret, so a registry error
  that echoes the request payload back can no longer leak the token into
  terminal output or JSON logs.
- Apply the same bounding/escaping to list_all_nodes() and install_node(),
  which log the full str(e) — an attacker-controlled registry could
  otherwise forge log lines or flood CI logs.
- Fix the adjacent set-literal quirk in the publish fallback:
  display_error_message({str(e)}) rendered as {'...'} instead of a plain
  string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mattmillerai
mattmillerai force-pushed the matt/be-3271-registry-api-typed-errors branch from 55d620c to ab54fba Compare July 22, 2026 19:42

@skishore23 skishore23 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The typed-error work is right and the hardening is better than the ticket asked for. One conflict to resolve before merge, plus a design follow-up worth filing.

Conflict: main grew its own typed registry exception

comfy_cli/registry/__init__.py conflicts because main added NodeFetchError after this branch opened:

HEAD:        from .api import RegistryAPI, RegistryAPIError
origin/main: from .api import NodeFetchError, RegistryAPI

They're complementary, not duplicative — I checked which sites each covers on current main:

site main this PR
get_node (:169) NodeFetchError untouched
publisher-id / project-name validation (:61,:64) bare Exception RegistryAPIError
publish (:105) bare Exception RegistryAPIError
list (:120) bare Exception RegistryAPIError
install (:146) bare Exception RegistryAPIError

So together all six are typed. Resolution is a union export — keep both names in the import and both in __all__. I did that locally: 122 passed. (The one failure, test_restore_snapshot_fast_deps.py::test_default_omitted_does_not_force_uv_compile, reproduces on plain origin/main — pre-existing, unrelated.)

Follow-up worth filing: the two exceptions don't compose

After the union resolution the registry exposes two typed errors with different attribute names and no common base:

class NodeFetchError(Exception):    def __init__(self, message, status_code=None)
class RegistryAPIError(Exception):  def __init__(self, message, *, status=None, body=None)

Consequences: except RegistryAPIError silently misses get_node failures, and a caller reading .status on a NodeFetchError gets AttributeError. Both classes exist for the same reason — "the registry said no, here's the HTTP status."

Cheapest fix is making NodeFetchError a subclass of RegistryAPIError that maps status_codestatus (keeping status_code as a property for existing callers). Then except RegistryAPIError is the one true handler. Not a blocker for this PR — main created the divergence — but it should be closed before a third variant appears.

Verified

The sanitize_error_body hardening actually works — exercised it directly:

redacts PAT : error token=***REDACTED*** denied
escapes CRLF: 'line1\\r\\nFAKE LOG LINE'
bounds len  : True  (MAX_ERROR_BODY_CHARS = 2000)

The ordering is correct and non-obvious: redact → escape → truncate. Truncating first could split a secret and leave a partial token visible; escaping first wouldn't change the match but would shift offsets. Worth a comment saying the order is load-bearing, since a future refactor could reorder it harmlessly-looking.

Also good that the PAT is passed as a secret only at the publish site (api.py:133), where the token is actually in scope — the list/install sites (:153, :184) correctly pass no secrets rather than threading a token they don't have.

Everything else: 105 passed as-authored; registry-install exiting 1 on a registry failure is a correctness fix and the narrow scoping (only RegistryAPIError takes the new path) is the right amount of caution; catching RegistryAPIError before the existing broad except preserves all other behavior.

The "CI red is external" section is now stale in your favor — all checks on this PR are currently green, so the tomlkit/pydantic_core triage can come out of the squash message.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Aug 4, 2026
…-api-typed-errors

# Conflicts:
#	comfy_cli/registry/__init__.py
Merging origin/main pulled in an `output.renderer` import for
get_renderer alongside this branch's existing `output` import of the
same re-exported name, tripping ruff's F811 redefinition check.
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 Reviews-loop pass: conflict resolved, CI green.

  • Merge conflict (skishore23's review): merged current main in — the comfy_cli/registry/__init__.py conflict resolved as the union export you described (both NodeFetchError and RegistryAPIError in the import + __all__); git's textual merge picked that up cleanly on its own since the two branches touched different lines.
  • Merge-induced lint break: the merge also combined two independent get_renderer imports (from comfy_cli.output import get_renderer from this branch's side, from comfy_cli.output.renderer import get_renderer from main's side — same re-exported symbol) into comfy_cli/command/custom_nodes/command.py, tripping ruff F811. Dropped the redundant one (d56b37e).
  • Full local suite on the merged tree: 4348 passed, 36 skipped, 0 failed. All PR checks now green, including build and test (Windows) — the tomlkit/pydantic_core external-CI issues noted earlier in this thread are resolved on current main.
  • On the NodeFetchError/RegistryAPIError composability follow-up you flagged: agreed it's a real design smell, but I checked both get_node call sites (node_deps.py, outdated.py) and neither uses except RegistryAPIError — both catch broad except Exception, so nothing today actually hits the "silently misses get_node failures" case. Since it's latent rather than an active bug, I'm leaving it as a reply rather than opening a ticket; worth revisiting if/when a caller starts narrowing to except RegistryAPIError near a get_node call.

mergeStateStatus is now CLEAN / MERGEABLE. Still review-gated — leaving for a human to merge.

…-api-typed-errors

# Conflicts:
#	tests/comfy_cli/command/nodes/test_node_install.py
…E-3271)

main independently added an error_codes.py entry for `node_install_failed`
(the `node install --exit-on-fail` / cm-cli path) while this branch was
also using that code for `node registry-install`'s registry-fetch failures.
The merge combined both entries under the same code, tripping
test_no_duplicate_codes and colliding two semantically distinct failure
modes. Rename the registry-install side to `registry_install_failed` so
both stay distinguishable machine-readable codes.
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 Reviews-loop pass: merge conflict resolved, CI green.

  • Merge conflict: main moved again since the last pass; rebased by merging origin/main. Textual conflict was only an import-order collision in tests/comfy_cli/command/nodes/test_node_install.py (both branches added an import on the same line) — resolved by keeping both.
  • Semantic collision caught by the test suite, not the merge: main independently added an error_codes.py entry for node_install_failed (the comfy node install --exit-on-fail / cm-cli path, BE-6634) while this branch already used that same code for comfy node registry-install's registry-fetch failures. The textual merge combined both entries under one code with no conflict marker, but test_no_duplicate_codes caught it. Renamed this branch's side to registry_install_failed (error_codes.py, the 3 call sites in command.py's registry_install, and the corresponding test assertions) — the cm-cli path keeps node_install_failed untouched.
  • Full local suite on the merged+fixed tree: 4535 passed, 36 skipped, 0 failed. ruff check / ruff format --diff clean (verified against CI's pinned ruff==0.15.15 — the repo's uv.lock-pinned ruff==0.12.7 flags stale UP038 findings that 0.15.15 no longer raises; not a real issue).
  • Review threads: all already resolved from prior passes. Nothing new from CodeRabbit (still rate-limited, non-blocking) or Cursor.

All PR checks green (build included this time). mergeStateStatus is CLEAN / MERGEABLE. Still review-gated — leaving for a human to merge.

@mattmillerai
mattmillerai merged commit 2a6a7d7 into main Aug 10, 2026
17 checks passed
@mattmillerai
mattmillerai deleted the matt/be-3271-registry-api-typed-errors branch August 10, 2026 18:52
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants