-
Notifications
You must be signed in to change notification settings - Fork 152
fix(assets): error on a non-object 2xx body from assets library ensure
#854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,10 +123,24 @@ def ensure_cmd( | |
| resource_id=hash, | ||
| ) from e | ||
|
|
||
| b = body or {} | ||
| # Unlike `ls`, an EMPTY body is an error here, not an empty result: a POST | ||
| # cannot confirm the borrow without one, and the server contract | ||
| # (`AssetCreated`) always returns an object. `http_request` also collapses | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — The comment's claim that every unparseable body collapses to Raised by 1 of 6 reviewers (gpt-5.6-sol-max edge-case). |
||
| # an unparseable body (an HTML error page from a proxy, say) to `None`, so | ||
| # `NoneType` covers both — otherwise they emit `{"ok": true}` with a null id | ||
|
Comment on lines
+129
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
python - <<'PY'
import json
try:
json.loads(b"\xff")
except UnicodeDecodeError:
raise SystemExit(0)
except Exception as exc:
raise SystemExit(f"unexpected exception: {type(exc).__name__}")
else:
raise SystemExit("expected a decoding failure")
PYRepository: Comfy-Org/comfy-cli Length of output: 157 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- changed parser and caller ---'
sed -n '1,95p' comfy_cli/command/cloud_http.py
sed -n '105,155p' comfy_cli/command/assets_library.py
printf '%s\n' '--- relevant tests and runtime declarations ---'
rg -n -S 'http_request|ensure_cmd|JSONDecodeError|UnicodeDecodeError|requires-python|python_requires|python-version' \
pyproject.toml setup.cfg setup.py .github comfy_cli tests 2>/dev/null | head -200Repository: Comfy-Org/comfy-cli Length of output: 25580 🤖 get_repo_knowledge executed:
Length of output: 4556 🏁 Script executed: #!/bin/bash
set -eu
cat -n comfy_cli/command/assets_library.py | sed -n '80,145p'
printf '%s\n' '--- tests directly covering cloud_http/assets_library ---'
rg -n -S 'assets_library|command.cloud_http|http_request\(' tests/comfy_cli | head -120Repository: Comfy-Org/comfy-cli Length of output: 4546 Handle invalid UTF-8 before relying on
Catch 🤖 Prompt for AI Agents |
||
| # and the caller's own hash echoed back as if the borrow had happened. | ||
| if not isinstance(body, dict): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 High — The guard checks only the container type, so the fake success it was written to prevent is still reachable: any 2xx JSON object lacking Raised by 6 of 6 reviewers (claude-opus-5-thinking-max adversarial, gpt-5.6-sol-max adversarial, kimi-k3-high adversarial, claude-opus-5-thinking-max edge-case, gpt-5.6-sol-max edge-case, kimi-k3-high edge-case). |
||
| renderer.error( | ||
| code="cloud_http_error", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — Raised by 2 of 6 reviewers (claude-opus-5-thinking-max adversarial, claude-opus-5-thinking-max edge-case). |
||
| message="unexpected response from /api/assets/from-hash (expected a JSON object describing the asset)", | ||
| hint="the borrow could not be confirmed; retry, and check whether a proxy is intercepting the request", | ||
| details={"operation": "ensure", "hash": hash, "got_type": type(body).__name__}, | ||
| ) | ||
| raise typer.Exit(code=1) | ||
|
|
||
| payload = { | ||
| "id": b.get("id"), | ||
| "hash": b.get("hash", hash), | ||
| "id": body.get("id"), | ||
| "hash": body.get("hash", hash), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Low — Raised by 2 of 6 reviewers (claude-opus-5-thinking-max edge-case, kimi-k3-high edge-case). |
||
| "created_new": status == 201, | ||
| } | ||
| renderer.emit(payload, command="assets library ensure", where="cloud") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,35 @@ def _fake(req, timeout=None): | |
| return calls | ||
|
|
||
|
|
||
| def _patch_urlopen_raw(monkeypatch: pytest.MonkeyPatch, raw: bytes, status: int = 201): | ||
| """Serve ``raw`` verbatim, unlike ``_patch_urlopen`` which JSON-encodes it. | ||
|
|
||
| Needed for the bodies a real proxy/gateway returns — an HTML error page, | ||
| nothing at all — which never survive a ``json.dumps`` round trip. | ||
| """ | ||
| calls: list[dict] = [] | ||
|
|
||
| class _Resp: | ||
| def __init__(self): | ||
| self.status = status | ||
|
|
||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, *a): | ||
| return False | ||
|
|
||
| def read(self, n=None): | ||
| return raw | ||
|
|
||
| def _fake(req, timeout=None): | ||
| calls.append({"url": req.full_url, "method": req.get_method(), "body": req.data}) | ||
| return _Resp() | ||
|
|
||
| monkeypatch.setattr("urllib.request.urlopen", _fake) | ||
| return calls | ||
|
|
||
|
|
||
| class TestEnsure: | ||
| def test_404_is_asset_not_found_not_workflow_not_found(self, cloud_target, monkeypatch, capsys): | ||
| _patch_urlopen(monkeypatch, _http_error(404)) | ||
|
|
@@ -105,6 +134,44 @@ def test_404_is_asset_not_found_not_workflow_not_found(self, cloud_target, monke | |
| assert err["details"]["hash"] == "comfyorg_logo.png" | ||
| assert err["details"]["operation"] == "ensure" | ||
|
|
||
| def test_non_object_body_is_cloud_http_error(self, cloud_target, monkeypatch, capsys): | ||
| # Pre-fix: `AttributeError: 'list' object has no attribute 'get'` and no envelope at all. | ||
| _patch_urlopen(monkeypatch, [1, 2, 3]) | ||
| env = _run(["ensure", "--hash", "a" * 64, "--where", "cloud"], capsys) | ||
| assert env["ok"] is False | ||
| assert env["error"]["code"] == "cloud_http_error" | ||
| assert error_codes.is_registered(env["error"]["code"]) | ||
| assert env["error"]["details"]["got_type"] == "list" | ||
| assert env["error"]["details"]["operation"] == "ensure" | ||
| assert "created_new" not in json.dumps(env) | ||
|
Comment on lines
+137
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert the complete failure contract for invalid responses. When an invalid body is returned, assert the command exit code is Return the This follows the PR objective that invalid responses must report the received type and exit with status 1. Also applies to: 148-156, 158-165, 167-174 🧰 Tools🪛 ast-grep (0.45.2)[info] 145-145: use jsonify instead of json.dumps for JSON output (use-jsonify) 🪛 Pylint (4.0.7)[convention] 137-137: Missing function or method docstring (C0116) [warning] 137-137: Redefining name 'cloud_target' from outer scope (line 36) (W0621) [warning] 137-137: Unused argument 'cloud_target' (W0613) 🤖 Prompt for AI Agents |
||
|
|
||
| def test_unparseable_body_is_cloud_http_error_not_fake_success(self, cloud_target, monkeypatch, capsys): | ||
| # Pre-fix: `{"ok": true, "data": {"id": null, "hash": <the caller's own hash>, "created_new": true}}` | ||
| # — a borrow the server never confirmed, reported as done. | ||
| _patch_urlopen_raw(monkeypatch, b"<html><body>502 Bad Gateway</body></html>") | ||
| env = _run(["ensure", "--hash", "a" * 64, "--where", "cloud"], capsys) | ||
| assert env["ok"] is False | ||
| assert env["error"]["code"] == "cloud_http_error" | ||
| assert error_codes.is_registered(env["error"]["code"]) | ||
| assert "created_new" not in json.dumps(env) | ||
|
|
||
| def test_empty_body_is_cloud_http_error(self, cloud_target, monkeypatch, capsys): | ||
| # A POST with no body cannot confirm the borrow: unlike `ls`, empty is an error here. | ||
| _patch_urlopen_raw(monkeypatch, b"", status=200) | ||
| env = _run(["ensure", "--hash", "a" * 64, "--where", "cloud"], capsys) | ||
| assert env["ok"] is False | ||
| assert env["error"]["code"] == "cloud_http_error" | ||
| assert error_codes.is_registered(env["error"]["code"]) | ||
| assert "created_new" not in json.dumps(env) | ||
|
|
||
| def test_whitespace_body_is_cloud_http_error(self, cloud_target, monkeypatch, capsys): | ||
| _patch_urlopen_raw(monkeypatch, b"\n") | ||
| env = _run(["ensure", "--hash", "a" * 64, "--where", "cloud"], capsys) | ||
| assert env["ok"] is False | ||
| assert env["error"]["code"] == "cloud_http_error" | ||
| assert error_codes.is_registered(env["error"]["code"]) | ||
| assert "created_new" not in json.dumps(env) | ||
|
|
||
| def test_401_is_still_cloud_unauthorized(self, cloud_target, monkeypatch, capsys): | ||
| _patch_urlopen(monkeypatch, _http_error(401)) | ||
| env = _run(["ensure", "--hash", "a" * 64, "--where", "cloud"], capsys) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 Low — Worth checking the sibling
lscommand in this file, which reportedly still does(body or {}).get("assets"): a bare JSON array (a common list-endpoint shape) is truthy and would raiseAttributeError: 'list' object has no attribute 'get'with no envelope at all — the same pre-fix failure the newensuretest documents. An HTML proxy page there also collapses toNoneand reports{"ok": true, "count": 0}, a fake "library is empty" that could drive a caller to re-upload.Raised by 1 of 6 reviewers (claude-opus-5-thinking-max adversarial).