Skip to content

Commit f20a036

Browse files
committed
fix: harden media URLs and portable setup
1 parent 950b500 commit f20a036

5 files changed

Lines changed: 146 additions & 13 deletions

File tree

README.md

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,28 @@ Every live public model on fal became a node: ~1,400 auto-generated nodes built
5151

5252
## Installation
5353

54-
1. Navigate to your ComfyUI custom nodes directory:
54+
The recommended installation method is **ComfyUI Manager**: search for
55+
`ComfyUI-fal-API`, install it, and restart ComfyUI. Manager installs the
56+
dependencies into the same Python environment ComfyUI uses.
57+
58+
For a manual installation:
59+
60+
1. Navigate to your ComfyUI custom-nodes directory and clone this repository:
5561
```
5662
cd custom_nodes
57-
```
58-
2. Clone this repository:
59-
```
6063
git clone https://github.com/gokayfem/ComfyUI-fal-API.git
64+
cd ComfyUI-fal-API
65+
```
66+
2. Install the dependencies with **ComfyUI's Python**, not an unrelated system
67+
`pip`:
6168
```
62-
3. Install the required dependencies:
69+
python -m pip install -r requirements.txt
6370
```
64-
pip install -r requirements.txt
71+
From the root of **ComfyUI Windows Portable**, use its embedded interpreter:
72+
```powershell
73+
.\python_embeded\python.exe -m pip install -r .\ComfyUI\custom_nodes\ComfyUI-fal-API\requirements.txt
6574
```
66-
4. Configure your API key (below) and restart ComfyUI. Curated nodes appear under the **FAL** category, auto-generated nodes under **FAL/Models/<category>** (e.g. `FAL/Models/text-to-image`), and hand-picked models under **FAL/Featured** — or just search for any model by name.
75+
3. Configure your API key (below) and restart ComfyUI. Curated nodes appear under the **FAL** category, auto-generated nodes under **FAL/Models/<category>** (e.g. `FAL/Models/text-to-image`), and hand-picked models under **FAL/Featured** — or just search for any model by name.
6776

6877
## Configuration
6978

@@ -169,12 +178,23 @@ The full LoRA-training pipeline needs nothing else: Load Image Folder → Batch
169178
```
170179
cd custom_nodes/ComfyUI-fal-API
171180
git pull
172-
pip install -r requirements.txt
173-
```
174-
3. If you're using ComfyUI Windows Portable, you may need to install fal-client manually:
181+
python -m pip install -r requirements.txt
175182
```
176-
ComfyUI_windows_portable>.\python_embeded\python.exe -m pip install fal-client
183+
3. **Windows Portable Python is blocked or no longer starts after installing a node?**
184+
Do not keep rerunning `pip`. From the portable root, first check the exact
185+
interpreter and dependency state:
186+
```powershell
187+
.\python_embeded\python.exe -c "import sys; print(sys.executable); print(sys.version)"
188+
.\python_embeded\python.exe -m pip check
177189
```
190+
This project installs Python packages only; it does not replace or modify
191+
`python.exe`. If the first command itself is blocked or the executable was
192+
quarantined, review Windows Security **Protection history**. Restore a file
193+
only when the portable archive came from the official ComfyUI release, or
194+
re-extract a clean official portable build and move your `models`, `input`,
195+
`output`, and `user` data across. Avoid disabling antivirus globally. Then
196+
reinstall this node through ComfyUI Manager, or use the exact embedded-
197+
interpreter requirements command from the Installation section.
178198
4. **Dynamic nodes not appearing?** Check the ComfyUI console for a line like `Registered N dynamic fal nodes` at startup. If it says the nodes are disabled, remove `enabled = false` from the `[dynamic_nodes]` section of your `config.ini` (and check the `categories` filter isn't excluding what you're looking for). Any registry loading error is also printed there.
179199
5. **`VIDEO` output is `None` or video sockets are missing?** Update ComfyUI — native `VIDEO`/`AUDIO` types require a recent ComfyUI version.
180200
6. **API calls failing?** Failed fal requests raise visible errors that include fal's actual error message (validation issues, content policy, quota). Read the error text in ComfyUI — it usually tells you exactly which parameter to fix.

nodes/utils/media.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ def _is_http_url(value: str) -> bool:
3333
return value.startswith(("http://", "https://"))
3434

3535

36+
def _require_http_url(value: Any, operation: str) -> str:
37+
"""Return a normalized HTTP(S) URL or raise an actionable fal error.
38+
39+
``requests`` otherwise turns values such as ``"E"`` (historically the
40+
first character of an error string routed through a list output) into a
41+
cryptic ``MissingSchema`` exception. Validate at the media boundary so
42+
the bad upstream value and the responsible operation remain visible.
43+
"""
44+
url = str(value or "").strip()
45+
parsed = urlparse(url)
46+
if parsed.scheme.lower() not in {"http", "https"} or not parsed.netloc:
47+
preview = repr(url if len(url) <= 120 else f"{url[:117]}...")
48+
raise FalApiError(
49+
operation,
50+
f"Expected an HTTP(S) media URL, got {preview}. "
51+
"The upstream generation may have failed, or a non-URL output "
52+
"may be connected to a media input.",
53+
)
54+
return url
55+
56+
3657
def _suffix_from_url(url: str, default: str) -> str:
3758
"""Derive a file suffix from a URL path, falling back to a default."""
3859
suffix = os.path.splitext(urlparse(url).path)[1]
@@ -164,9 +185,15 @@ class MediaUtils:
164185
http(s) URL inputs pass through untouched.
165186
"""
166187

188+
@staticmethod
189+
def require_http_url(value: Any, operation: str = "media-download") -> str:
190+
"""Public validation hook for legacy nodes that return media URLs."""
191+
return _require_http_url(value, operation)
192+
167193
@staticmethod
168194
def download_url_to_temp(url: str, suffix: str) -> str:
169195
"""Stream a URL to a temp file and return its local path."""
196+
url = _require_http_url(url, "media-download")
170197
temp_path: str | None = None
171198
try:
172199
with requests.get(url, stream=True, timeout=_DOWNLOAD_TIMEOUT) as resp:

nodes/video_node.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2770,8 +2770,16 @@ def generate_video(self, prompt, image, duration, end_image=None, negative_promp
27702770
"fal-ai/bytedance/seedance/v1/pro/image-to-video", arguments, variations
27712771
)
27722772

2773-
# Return list of video URLs
2774-
return ([r["video"]["url"] for r in results],)
2773+
# Validate before exposing URLs to downstream loaders. Older error
2774+
# paths could leak an error string through this list output; a
2775+
# downstream loader would then receive its first character ("E")
2776+
# and raise requests.exceptions.MissingSchema.
2777+
endpoint = "fal-ai/bytedance/seedance/v1/pro/image-to-video"
2778+
video_urls = [
2779+
MediaUtils.require_http_url(r["video"]["url"], endpoint)
2780+
for r in results
2781+
]
2782+
return (video_urls,)
27752783

27762784
except Exception as e:
27772785
return ApiHandler.handle_video_generation_error(

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,8 @@ def factory_mod():
7676
@pytest.fixture(scope="session")
7777
def errors_mod():
7878
return _submodule("nodes.utils.errors")
79+
80+
81+
@pytest.fixture(scope="session")
82+
def media_mod():
83+
return _submodule("nodes.utils.media")

tests/test_media_urls.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Regression tests for malformed media URLs and legacy Seedance output."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import pytest
8+
9+
10+
def test_download_rejects_missing_schema_before_requests(monkeypatch, media_mod):
11+
called = False
12+
13+
def unexpected_get(*_args, **_kwargs):
14+
nonlocal called
15+
called = True
16+
raise AssertionError("requests.get must not receive a malformed URL")
17+
18+
monkeypatch.setattr(media_mod.requests, "get", unexpected_get)
19+
20+
with pytest.raises(media_mod.FalApiError, match=r"Expected an HTTP\(S\) media URL"):
21+
media_mod.MediaUtils.download_url_to_temp("E", ".mp4")
22+
23+
assert called is False
24+
25+
26+
def test_url_validator_normalizes_whitespace(media_mod):
27+
assert (
28+
media_mod.MediaUtils.require_http_url(" https://fal.media/video.mp4 ")
29+
== "https://fal.media/video.mp4"
30+
)
31+
32+
33+
def test_seedance_pro_rejects_non_url_result(pack, monkeypatch):
34+
node_cls = pack.NODE_CLASS_MAPPINGS["SeedanceProImageToVideo_fal"]
35+
module = sys.modules[node_cls.__module__]
36+
37+
monkeypatch.setattr(
38+
module.ImageUtils,
39+
"upload_image",
40+
staticmethod(lambda _image: "https://fal.media/input.png"),
41+
)
42+
monkeypatch.setattr(
43+
module.ApiHandler,
44+
"submit_multiple_and_get_results",
45+
staticmethod(lambda *_args, **_kwargs: [{"video": {"url": "E"}}]),
46+
)
47+
48+
with pytest.raises(module.FalApiError, match=r"Expected an HTTP\(S\) media URL"):
49+
node_cls().generate_video("prompt", object(), "5")
50+
51+
52+
def test_seedance_pro_returns_validated_url_list(pack, monkeypatch):
53+
node_cls = pack.NODE_CLASS_MAPPINGS["SeedanceProImageToVideo_fal"]
54+
module = sys.modules[node_cls.__module__]
55+
56+
monkeypatch.setattr(
57+
module.ImageUtils,
58+
"upload_image",
59+
staticmethod(lambda _image: "https://fal.media/input.png"),
60+
)
61+
monkeypatch.setattr(
62+
module.ApiHandler,
63+
"submit_multiple_and_get_results",
64+
staticmethod(
65+
lambda *_args, **_kwargs: [
66+
{"video": {"url": "https://fal.media/output.mp4"}}
67+
]
68+
),
69+
)
70+
71+
assert node_cls().generate_video("prompt", object(), "5") == (
72+
["https://fal.media/output.mp4"],
73+
)

0 commit comments

Comments
 (0)