Skip to content

fix: comfy download copies on-disk local outputs instead of refusing them (BE-2190)#485

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-2190-download-local-outputs
Open

fix: comfy download copies on-disk local outputs instead of refusing them (BE-2190)#485
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-2190-download-local-outputs

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

If you run a workflow locally and then try to comfy download its result, ComfyUI already wrote the image to your disk — there's no web address to fetch. But download is a web-fetcher with a safety guard that refuses anything that isn't an http(s):// link, so it choked on the plain file path with refusing to download from non-HTTP URL: /…/output/foo.png. This teaches download to notice "hey, that's already a file on disk" and just copy it into your output folder instead of trying to fetch it — while keeping the safety guard fully in force for real remote URLs.

What & why

Fixes #480.

A comfy run --where local --wait job records bare absolute output paths (run/execution.py::format_image_path returns an on-disk path for loopback hosts). comfy download fed those through _assert_download_url, whose anti-SSRF guard rejects any non-http(s) URL → [download_failed]: refusing to download from non-HTTP URL.

The fix (additive — SSRF guard untouched for real URLs)

  • New _local_source_path(url) returns a Path for a bare absolute filesystem path or a file:// URL with no remote host, and None for anything carrying a network scheme (http/https/ftp/gopher/data/…). A file:// URL pointing at a remote host returns None too, so it still hits the guard.
  • In the download loop: when the source is local, shutil.copyfile it into --out-dir (no HTTP request, so the SSRF guard is irrelevant — there is no host to forge a request to). Everything else still runs through _assert_download_url exactly as before.
  • Extension fix (latent bug called out in comfy download refuses local runs: "refusing to download from non-HTTP URL" for on-disk output paths from comfy run --where local --wait #480): a bare path has no ?filename= query param, so the old code mislabeled every local output .png. The copied file now takes its extension from the real source file.
  • The destination-symlink refusal now runs for both branches (copy and fetch); a missing local source produces a clean download_failed envelope, not a traceback.

Acceptance criteria

  1. comfy download <prompt_id> --where local succeeds for an on-disk output, copying it into --out-dir and returning its path.
  2. _assert_download_url still rejects non-http(s) URLs on the remote/download path — tests cover both the local-copy branch and a rejected non-http URL (ftp://, remote-host file://, gopher://, data:) end-to-end through the loop.
  3. ✅ Copied local outputs keep their real extension (.webp, .mp4, …), not hardcoded .png.

Testing

  • Added TestLocalOutputCopy (bare path copy, extension preservation, file:// URI, missing-source error) and TestSSRFGuardIntact (guard still rejects non-http; remote-host file:// not treated as local; non-http rejected end-to-end through the loop).
  • pytest tests/comfy_cli/command/1066 passed, 1 skipped.
  • ruff check + ruff format --check clean.

Judgment calls

  • Local detection uses Path(url).is_absolute() (platform-aware for POSIX /… and Windows C:\…) after ruling out the file:// scheme. Network URLs are never absolute paths, so they always fall through to the SSRF guard. A protocol-relative //host/path would be treated as a local read (not a network fetch) and simply fail is_file() with a clean error — not an SSRF vector, and not a form the producer emits.
  • The emitted files[].url for a local output is the bare source path (provenance); the transfer schema types url as an unconstrained string, so this validates.

🤖 Generated with Claude Code

…them (BE-2190)

A LOCAL run (comfy run --where local --wait) records bare absolute output
paths, but comfy download is an HTTP fetcher whose anti-SSRF guard rejected
any non-http(s) URL, so downloading a local job failed with
'refusing to download from non-HTTP URL'.

Detect a bare absolute path / file:// URL in the download loop and
shutil.copyfile it into --out-dir before the HTTP guard runs; real URLs
still go through _assert_download_url, so SSRF protection is unchanged. The
copied file's extension is derived from the source file (a bare path has no
?filename= param, so the prior code mislabeled every local output .png).
@mattmillerai mattmillerai added agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review labels Jul 2, 2026
@mattmillerai mattmillerai marked this pull request as ready for review July 2, 2026 04:48
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 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: 7 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: 78957fe3-a73c-406e-a858-4e85996f4a95

📥 Commits

Reviewing files that changed from the base of the PR and between a628223 and eb3d157.

📒 Files selected for processing (2)
  • comfy_cli/command/transfer.py
  • tests/comfy_cli/command/test_transfer_download.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-2190-download-local-outputs
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-2190-download-local-outputs

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

@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 6 finding(s).

Severity Count
🟠 High 2
🟡 Medium 2
🟢 Low 1
⚪ Nit 1

Panel: 8/8 reviewers contributed findings.

Comment thread comfy_cli/command/transfer.py Outdated
Comment thread comfy_cli/command/transfer.py
Comment thread comfy_cli/command/transfer.py Outdated
Comment thread comfy_cli/command/transfer.py Outdated
Comment thread comfy_cli/command/transfer.py
Comment thread comfy_cli/command/transfer.py Outdated
…2190)

Address review findings on the copy-from-disk download branch:

- Gate the copy-from-disk branch on the job's own `where == "local"`
  marker, not on URL shape alone. `output_urls` can arrive from
  untrusted metadata (piped stdin envelope, cloud/remote API record), so
  a bare path / file:// URL from a non-local job must fall through to the
  SSRF guard instead of being copied off disk (was: 6/8 reviewers).
- Reject UNC / network paths (\\host\share, //host/share) in
  _local_source_path so is_file() can't trigger an outbound NTLM-leaking
  SMB connection on Windows.
- Use parsed.hostname (not netloc) for the loopback check so a port or
  IPv6 brackets (file://localhost:8080/…, file://[::1]/…) don't defeat it.
- Wrap shutil.copyfile in try/except: OSError and SameFileError now
  surface as structured download_failed envelopes instead of tracebacks.
- Apply the HTTP branch's 10 GB size cap to the local copy too.

Add regression tests for the local-job gate, UNC rejection, and
loopback-with-port file:// URIs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

comfy download refuses local runs: "refusing to download from non-HTTP URL" for on-disk output paths from comfy run --where local --wait

2 participants