Skip to content

fix(models): emit envelope/1 errors on every model download failure (BE-4217) - #581

Merged
mattmillerai merged 4 commits into
mainfrom
matt/be-4217-model-download-error-envelopes
Jul 30, 2026
Merged

fix(models): emit envelope/1 errors on every model download failure (BE-4217)#581
mattmillerai merged 4 commits into
mainfrom
matt/be-4217-model-download-error-envelopes

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

If you ask the CLI to download a model and it fails, it should say so in a way a
program can understand — a JSON error with a code — and it should exit with a
failure code.

Two of the failure cases didn't do that. If the file was already there, or if
Hugging Face said "you need a token," the CLI printed a message and exited 0
— i.e. "success." The local MCP wrapper sees "exit 0, no JSON" and synthesizes
a success envelope
, so an agent was told "download complete" for a download
that never happened. Other failures exited 1 but with no JSON at all, so the MCP
could only forward a raw stderr tail with code=None.

Now every failure emits one envelope/1 error object with a real error.code
and exits 1. Success is untouched (prints + exit 0, no envelope) because the
MCP's synthesizer depends on that meaning success.

What changed

All failure paths in comfy_cli/command/models/models.py download funnel
through one _download_failure() helper (emit renderer.error(...), return the
typer.Exit(1) to raise):

Failure Code Before
Target file already exists model_file_exists red print, exit 0
Hugging Face 401 and no token hf_unauthorized print advice, exit 0
No resolvable filename missing_argument bare typer.Exit(1), no message
Empty filename missing_argument unhandled DownloadException traceback
Transfer failed (DownloadException) download_failed red print + exit 1, no envelope
Local write failed (OSError) download_failed traceback
CivitAI metadata lookup failed / no primary file download_failed + details.stage="resolve" traceback
Hugging Face download or huggingface_hub install failed download_failed traceback

Registry (comfy_cli/error_codes.py): adds model_file_exists and
hf_unauthorized; widens the existing download_failed entry to cover model
downloads (reused rather than adding a fourth code, per the ticket's
"reuse an exact fit" instruction).

escape() around the DownloadException message is dropped: error_panel
builds its body from rich.text.Text, which is literal, so markup
metacharacters ([/], [id]) survive without a MarkupError. The existing
markup regression test still passes, and a new JSON-mode test asserts the raw
message reaches error.message byte-for-byte.

Acceptance criteria — verified live against the real CLI

Run in a temp workspace on this branch:

$ comfy --json model download --url .../NO_SUCH_FILE_404.bin \
    --relative-path models/checkpoints --filename x.safetensors
{"schema":"envelope/1","ok":false,"command":"model download",...,
 "error":{"code":"download_failed","message":"Failed to download file.\nFile not found on server (404)",...}}
exit=1                                                              # criterion 1 ✅

$ comfy --json model download --url <same file, already on disk> ...
 "error":{"code":"model_file_exists",...,"details":{"path":"…/direct.bin"}}
exit=1                                                              # criterion 2a ✅

$ comfy --json model download --url https://huggingface.co/meta-llama/Llama-2-7b-hf/resolve/main/config.json ...
 "error":{"code":"hf_unauthorized",...,"details":{"repo_id":"meta-llama/Llama-2-7b-hf"}}
exit=1                                                              # criterion 2b ✅

$ comfy --json model download --url <a real direct file URL> ...
exit=0, stdout empty, 35823 bytes on disk                           # criterion 3 ✅

tests/comfy_cli/output/test_error_code_registry.py passes (criterion 4 ✅),
as does the full suite: 2780 passed, 37 skipped; ruff check + ruff format
clean on the CI-pinned ruff 0.15.15.

Judgment call — the model_source_unknown hard stop was NOT shipped

The ticket's plan item 2 asked for a model_source_unknown code on the
else: print("Model source is unknown") branch, made to stop (exit 1)
instead of falling through.

That branch is not an error path — it is how a plain direct file URL is
downloaded.
Before shipping a capability denial I went and attempted the
capability by every path the CLI offers:

  • Live run: comfy --json model download --url https://raw.githubusercontent.com/Comfy-Org/comfy-cli/main/LICENSE --relative-path models/checkpoints --filename direct.bin → exit 0, 35823 bytes written to disk. This URL is neither civitai.com nor huggingface.co, so it takes exactly the branch the ticket wanted to turn into a dead end.
  • Existing tests on main: TestDownloadCommandDownloaderOption and TestDownloadCommandErrorHandling in tests/comfy_cli/command/models/test_models.py patch both host checks to False and then assert download_file is called and "Done in " is printed — the direct-URL path is already the tested contract.
  • The ticket's own acceptance criterion 1 passes a <404-url> (a direct URL) and requires error.code == "download_failed". A model_source_unknown stop would have produced model_source_unknown instead, so the plan item and the acceptance criterion are mutually exclusive.
  • The proposed hint text itself — "pass a direct file URL, a huggingface.co URL, or a civitai.com URL" — names direct file URLs as valid, i.e. the denial would have been denying a path it simultaneously recommended.

So the branch keeps working. What I did change is its misleading log line, which
is what made it read like an error path in the first place:
Model source is unknownModel source is unknown; treating <url> as a direct file URL
(stderr in --json mode, so no result-shape change). No model_source_unknown
code was registered — the registry test enforces both directions, so an
unraised code would fail CI anyway.

The underlying JSON-mode concern the plan item was reaching for — "there is no
interactive prompt to save it in --json mode" — is real, and it is handled at
the place it actually bites: when no filename can be resolved (prompt skipped or
cancelled), the command now errors with missing_argument + pass --filename
instead of a bare exit or a traceback.

Other notes for review

  • Intentional behavior change: file-exists now exits 1 in pretty mode too,
    not just --json. The ticket offered keeping exit 0 for TTY users but called
    exit-1-in-both "the simplest consistent choice," and acceptance criterion 2
    states it unqualified. A script that re-ran a download to no-op on an existing
    file will now see a failure exit — the friendly message is preserved (as the
    red error panel with the same File already exists: <path> text).
  • Beyond the ticket's six paths, three more failure branches were converted
    because they violated the same invariant by crashing with a traceback and no
    envelope: CivitAI metadata resolution, the Hugging Face download/huggingface_hub
    install, and a local OSError on write. Each is a bounded try/except that
    re-raises as typer.Exit(1) after emitting the envelope; the broad
    except Exception clauses cannot swallow Ctrl-C (cancellation.py raises
    KeyboardInterrupt, a BaseException).
  • Not converted: the print("Error parsing CivitAI model URL") inside
    check_civitai_url's except. It is a pure predicate returning a tuple, and
    every outcome downstream of it now lands on a covered path (direct download →
    download_failed, or CivitAI resolve → download_failed), so the invariant
    holds without changing that function's contract.
  • _download_failure's code is keyword-only on purpose: the registry test
    AST-scans for code="..." keyword arguments, so a positional call site would
    silently drop out of the both-ways registry enforcement.
  • New tests: tests/comfy_cli/command/models/test_model_download_json.py (12
    cases). They pin the renderer's machine/pretty streams to explicit buffers
    rather than relying on CliRunner's mix_stderr, which is a default in
    Click 8.1 and removed in 8.2 — so the "exactly one envelope on stdout, human
    logs on stderr" assertion holds on either Click.

`comfy --json model download` predated the renderer.error() envelope infra:
its failure paths either exited 1 with no envelope (so the local MCP saw
`code=None` and a raw stderr tail) or, worse, exited 0 with no envelope at
all — which the MCP's plain_ok synthesizer turns into a synthesized SUCCESS
for a download that never happened.

Every failure now funnels through a single `_download_failure()` helper that
emits an `envelope/1` error and exits 1:

- file already exists            -> model_file_exists  (was exit 0)
- Hugging Face 401, no token     -> hf_unauthorized    (was exit 0)
- no resolvable filename         -> missing_argument   (was a bare Exit(1)
                                    / an unhandled DownloadException)
- transfer failure               -> download_failed
- local write failure (OSError)  -> download_failed    (was a traceback)
- CivitAI metadata lookup fails  -> download_failed, details.stage=resolve
                                    (was a traceback)
- Hugging Face download / hub install fails -> download_failed (was a traceback)

Registers `model_file_exists` and `hf_unauthorized`, and widens the existing
`download_failed` entry to cover model downloads. The success path is
unchanged (prints + exit 0, no envelope) because the MCP synthesizer depends
on exit-0-no-envelope meaning success.

The ticket also proposed a `model_source_unknown` hard stop on the "Model
source is unknown" branch. That branch is NOT an error: it is how a plain
direct file URL is downloaded, verified live (a raw.githubusercontent.com URL
downloads 35823 bytes, exit 0) and covered by the pre-existing direct-URL
tests. Stopping there would break direct downloads and contradict the
ticket's own acceptance case (a 404 direct URL must yield download_failed),
so the branch keeps working and only its misleading log line was corrected.
@mattmillerai mattmillerai added agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review labels Jul 23, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 23, 2026 22:35
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

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: 41 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: a503219e-c0b4-4f0b-98ae-baa9b535695a

📥 Commits

Reviewing files that changed from the base of the PR and between 10e7b1c and fc4081e.

📒 Files selected for processing (3)
  • comfy_cli/command/models/models.py
  • tests/comfy_cli/command/models/test_model_download_json.py
  • tests/comfy_cli/command/test_model_download_background.py
📝 Walkthrough

Walkthrough

Changes

The model download command now validates filenames and CivitAI metadata, scrubs sensitive URL data, and routes download failures through structured error envelopes. Hugging Face filename handling and authorization errors were updated, while HTTP content-length parsing now tolerates invalid values.

Model download hardening

Layer / File(s) Summary
Download error contracts and helpers
comfy_cli/command/models/models.py, comfy_cli/error_codes.py
Centralized download failures, URL scrubbing, safe path validation, and expanded error-code definitions.
Metadata resolution and destination validation
comfy_cli/command/models/models.py
CivitAI lookup failures, unsafe metadata components, missing filenames, and existing destinations now follow structured error paths.
Transfer and Hugging Face handling
comfy_cli/command/models/models.py, comfy_cli/file_utils.py, tests/test_file_utils_network.py
Hugging Face and direct-download failures are enveloped, explicit filenames are honored, and malformed content lengths are treated as unknown sizes.
CLI contract and regression coverage
tests/comfy_cli/command/models/test_model_download_json.py, tests/comfy_cli/command/test_model_download_background.py
Tests cover JSON output, sanitization, path validation, metadata failures, authorization, overwrite refusal, background exits, and successful download behavior.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-4217-model-download-error-envelopes
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-4217-model-download-error-envelopes

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

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug Something isn't working labels Jul 23, 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 7 finding(s).

Severity Count
🟠 High 1
🟡 Medium 4
🟢 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/models/models.py Outdated
Comment thread comfy_cli/command/models/models.py
Comment thread comfy_cli/command/models/models.py Outdated
Comment thread comfy_cli/command/models/models.py Outdated
Comment thread comfy_cli/command/models/models.py
Comment thread comfy_cli/command/models/models.py Outdated
Comment thread tests/comfy_cli/command/models/test_model_download_json.py Outdated
…(BE-4217)

Follow-ups to the review panel on #581:

- Honour `--filename` on the *gated* Hugging Face path. `hf_hub_download`
  names the file after the repo path, so `--filename` was silently ignored
  there while the public-HF path (via `download_file`) honoured it. That split
  made the `local_filepath.exists()` guard inspect a path the branch never
  writes, and made the new `model_file_exists` hint ("pass `--filename`")
  impossible to act on. The result is now moved to the requested name.

- Reject path components that escape the workspace. `local_filename` (from
  `--filename`, or from the CivitAI API's `file["name"]`, which is the accepted
  default in non-interactive runs) and `basemodel` (`version["baseModel"]`) are
  joined into the destination without sanitisation; `pathlib` does not neutralise
  `..` or an absolute component. Both are now validated, with the hint pointing
  at `--relative-path` — the option that exists for choosing a directory, so the
  capability is redirected rather than removed.

- Scrub credentials out of the URL before it is echoed. CivitAI links carry the
  token as `?token=`; `tracking._scrub_value` already strips it before telemetry,
  and an error envelope is a louder channel than telemetry. Query, fragment and
  userinfo are dropped from every `details.url` and log line.

- Restore `escape()` on interpolated values. `print` is Rich's markup-parsing
  print, so a URL holding `[/]` (or an IPv6 literal like `http://[::1]/x`) raised
  MarkupError out of the *logging* — an uncaught crash with no envelope, which is
  the exact failure this command promises not to have.

- Guard `int(Content-Length)` in `_download_file_httpx`: a non-numeric header
  from a broken proxy raised ValueError past both handlers. Root-caused there,
  plus a catch-all backstop at the call site so no downloader failure can end the
  command without an envelope.

- Drop the ineffective `patch("comfy_cli.tracking.track_command", ...)` from the
  tests: `download` is decorated at import time, so patching the factory
  afterwards never rebinds the wrapper.

The HF auto-install targeting the workspace interpreter (Low finding) is left
as-is — it is pinned by tests/comfy_cli/test_models_python_resolution.py and
changing it is a separate decision; deferred to a follow-up.
@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:

  • BE-4272 — Make the huggingface_hub auto-install target the interpreter that will import it

@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.

LGTM

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 28, 2026
@bigcat88

Copy link
Copy Markdown
Contributor

This PR currently conflicts with main — GitHub reports mergeable: CONFLICTING, so it needs a rebase before I can review it and I'm skipping it in the current review sweep.

Please rebase (or merge main in) and I'll pick it up on the next pass. main moved a fair bit in the last day, including #614 (ANSI sanitisation across the pretty-print call sites) and #628 (the duplicate server_died error-code fix that had main red), so a refresh may also clear unrelated CI noise on this branch.

Resolves a semantic conflict with main's `--background` download feature
(#728d11c) and the version-switch invalid_argument hint tightening (#606):

- comfy_cli/command/models/models.py: combine main's needs_hf_auth/background
  restructuring with this branch's error-envelope hardening (hf_hub_download
  install/download/rename failures now all raise _download_failure, same as
  before the refactor moved).
- comfy_cli/error_codes.py: keep main's generalized path-segment hint (used by
  multiple commands, not just model download) while keeping this branch's
  detail about the CivitAI-sourced filename/basemodel case.
- tests/comfy_cli/command/test_model_download_background.py: main's
  TestSubmitFailsFast tests asserted the pre-envelope failure shape
  (DownloadException / bare print+return) for paths this branch converts to
  typer.Exit + envelope; updated to match the new (intended) behavior. The
  fail-fast invariant they exist to guard — nothing detaches before
  resolution succeeds — is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@comfy_cli/command/models/models.py`:
- Around line 568-572: Update the move condition in the download flow to use the
resolved local name/path, including names entered through the interactive
prompt, rather than checking only filename. Keep the existing local_filepath
comparison and move behavior, and ensure the existence guard and
model_file_exists hint refer to the same destination that the branch writes.

In `@tests/comfy_cli/command/models/test_model_download_json.py`:
- Around line 112-116: Update the test helper _invoke to accept
check_civitai_url and check_huggingface_url through its existing **patches
override mechanism, supplying the current mocked defaults and allowing
caller-provided patches to take precedence. Replace the hard-coded patches in
the with block, preserving existing behavior while enabling CivitAI and
HuggingFace tests to override these checks without rebuilding the patch stack.

In `@tests/comfy_cli/command/test_model_download_background.py`:
- Around line 515-556: Update all four failure-path tests in the background
download suite to capture the raised typer.Exit exception and assert its
exit_code is nonzero, rather than only asserting the exception type. Apply this
to the tests for unresolved filenames, existing destinations, unresolvable
filenames, and missing Hugging Face tokens while preserving their existing
output assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e42eb063-a778-4053-879d-e7ca03922f3b

📥 Commits

Reviewing files that changed from the base of the PR and between 46e6cf7 and 10e7b1c.

📒 Files selected for processing (6)
  • comfy_cli/command/models/models.py
  • comfy_cli/error_codes.py
  • comfy_cli/file_utils.py
  • tests/comfy_cli/command/models/test_model_download_json.py
  • tests/comfy_cli/command/test_model_download_background.py
  • tests/test_file_utils_network.py

Comment thread comfy_cli/command/models/models.py Outdated
Comment thread tests/comfy_cli/command/models/test_model_download_json.py Outdated
Comment thread tests/comfy_cli/command/test_model_download_background.py Outdated
…name (BE-4217)

Three CodeRabbit findings from the last review round:

- `models.py`: gate the post-`hf_hub_download` move on the *resolved*
  `local_filename` instead of `filename is not None`. The name can equally come
  from the interactive prompt (whose default is only a suggestion), so keying on
  the flag left the same split open via the prompt door: the artifact landed at
  the repo path while the `local_filepath.exists()` guard and its
  `model_file_exists` hint ("pass `--filename`") both described a path this
  branch never wrote. New regression test covers the prompted-name case, and it
  fails against the old condition.

- `test_model_download_json.py`: fold `check_civitai_url` /
  `check_huggingface_url` into `_invoke`'s `**patches` mechanism (defaults
  merged, caller wins) and convert the five tests that were rebuilding the whole
  patch stack by hand.

- `test_model_download_background.py`: pin `exit_code == 1` on the four
  `TestSubmitFailsFast` blocks. `typer.Exit` defaults to 0, so they passed even
  if a regression started exiting successfully on these failure paths — the very
  "exit 0 with no envelope" bug this PR exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

Rebase done — main is merged in (10e7b1c) and the branch is 0 behind, so mergeable is back to MERGEABLE. #614 and #628 are both in the merged tree here.

Since your note I also cleared the three remaining CodeRabbit threads in fc4081e (HF result now moves to the resolved filename rather than only when --filename is passed, plus two test-hardening items). All threads are resolved, full local suite green (3435 passed / 37 skipped), and there is no merge-queue ejection on record for this PR.

Ready for your pass whenever it suits, @bigcat88.

@mattmillerai
mattmillerai requested a review from bigcat88 July 29, 2026 18:52
@mattmillerai
mattmillerai merged commit 7320bf5 into main Jul 30, 2026
16 checks passed
@mattmillerai
mattmillerai deleted the matt/be-4217-model-download-error-envelopes branch July 30, 2026 06:32
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 30, 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 bug Something isn't working 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.

3 participants