feat(errors): add a typed AssetInUse so a delete conflict is catchable - #151
mattmillerai wants to merge 2 commits into
Conversation
`spec/openapi.yaml` documents two 409s — `hash_mismatch` on POST /v2/assets
and `asset_in_use` on DELETE /v2/assets/{id} — and only the first had a class.
An enveloped delete conflict therefore arrived as a bare ApiError/ComfyError
and a caller who wanted to handle just that case had to string-compare `.code`,
which is exactly the protocol detail the typed error surface exists to hide.
Adds AssetInUse at both layers (comfy_low.errors, comfy_sdk.exceptions) with
the matching _BY_CODE entry, exported from both packages the way the other
typed errors are. `asset_in_use` is not a Router bucket, so registering it in
comfy_sdk's table does not shadow any router-only retype.
Tested at the mapping level on both layers and end to end through the stub
server's DELETE route, including that a refused delete leaves the Asset
handle's id intact so the caller can retry once the hold clears.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: Comfy-Org/comfy-python-sdk/.coderabbit.yaml Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (10)
Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthroughThe SDKs add ChangesAsset deletion conflict handling
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant AssetDeletion as Asset deletion
participant TestServer as Test server
participant ComfyLow as comfy_low error mapper
participant ComfySDK as comfy_sdk error mapper
AssetDeletion->>TestServer: DELETE /assets/{id}
TestServer-->>AssetDeletion: HTTP 409 asset_in_use
AssetDeletion->>ComfyLow: Map asset_in_use response
ComfyLow-->>AssetDeletion: AssetInUse
AssetDeletion->>ComfySDK: Convert transport error
ComfySDK-->>AssetDeletion: AssetInUse
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 8 files. (2 skipped: 2 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 2 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| ⚪ Nit | 1 |
Panel: 6/6 reviewers contributed findings.
Resolves the textual conflicts and the semantic one underneath them. `CHANGELOG.md` and `src/comfy_sdk/__init__.py` were additive on both sides; both sides kept. `tests/test_error_mapping.py` was the real conflict. main's #132 landed a naming convention in that module -- the bare name is the `comfy_low` class and the `Sdk*` alias is the `comfy_sdk` one -- while this branch had imported `comfy_sdk.exceptions.HashMismatch` under the bare name. Keeping both import blocks would have compiled and shadowed main's `HashMismatch`, so main's own assertions at the protocol layer would silently have been checking the SDK class instead. The branch's tests now follow main's convention (`AssetInUse` / `SdkAssetInUse`, `HashMismatch` / `SdkHashMismatch`). Also from the merge: #132 dropped `409` from `_CODE_BY_STATUS` outright, which is what the panel's Medium finding asked for on this PR. No change needed here -- a code-less `409` already degrades to a bare `ApiError`, so the typed `AssetInUse` added here is reached only via the enveloped `error.code`. Two review fixes on top: - The route in `AssetInUse`'s docstring and in the `test_error_mapping.py` comment is `DELETE /api/v2/assets/{id}`. Every asset path in the tree and in `spec/openapi.yaml` carries the `/api` prefix; bare `/v2/...` is reserved for the Router surface on a different host. (Panel nit.) - `AssetInUse` was missing from `comfy_sdk.exceptions.__all__`. Found in self-review: the branch added it to both packages' `__all__` but not to the module's own, so `import *` skipped the class and doc tooling read it as private. `test_exception_modules.py` could not catch this -- its `_exported` helper unions `__all__` with `dir()` by design -- so a guard now asserts every class in `_BY_CODE` appears in `__all__`. Mutation-checked by removing the entry again. 982 passed, 9 skipped. ruff, ruff format, mypy src, codegen drift and public-repo-hygiene all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ELI-5
The API says "no" in two different ways when it returns a
409. Uploading bytes that don't match their declared hash is one (hash_mismatch); deleting an asset something still depends on is the other (asset_in_use). Only the first had a name you couldexcepton, so a caller who wanted to handle just the delete case had to writeif exc.code == "asset_in_use"— reaching past the typed errors into the wire protocol. This gives the second one a name too.What changed
comfy_low.errors.AssetInUse(ApiError)withcode = "asset_in_use", registered in that module's_BY_CODEbesideHashMismatch.comfy_sdk.exceptions.AssetInUse(ComfyError)with the matching_BY_CODEentry, exported fromcomfy_sdk(andAssetInUsefromcomfy_low) the way every other typed error is.DELETE /api/v2/assets/{id}route (sync + async), and a regression assert thathash_mismatchstill maps toHashMismatchand the two classes are siblings rather than one subclassing the other.CHANGELOG.mdunder[Unreleased] → Added— the release flow is tag-driven andpyproject.toml's version is a placeholder, so an already-tagged heading would have been wrong.Judgment calls
Registering
asset_in_useincomfy_sdk's_BY_CODEis the one line with blast radius, because that table is also a filter._router_only_class()returnsNonefor any code already in_BY_CODE, so adding a key removes that code from Router-bucket retyping. I checked before adding it:asset_in_useappears nowhere inrouter_exceptions.pyorspec/router-openapi.yaml— it is a v2-envelope code only — so nothing was shadowed. The router-spec contract check (scripts/check_drift.py) still reports all 15 buckets covered.The two 409s are siblings, not a hierarchy.
WorkflowFormatUisubclassesInvalidWorkflowbecause it is a narrower case of the same failure; a delete conflict and an upload hash conflict are unrelated failures that only share a status, soAssetInUseextends the base directly. A test pins that both ways, since making one catch the other later would be a silent behaviour change for existingexcept HashMismatchhandlers.I added a
reject_delete_in_useflag to the stub server rather than mocking httpx, per the repo's rule that the suite drivesserver.stateand has no network dependency.No capability is being denied by this diff, so the falsification discipline does not apply: it adds a class and a
_BY_CODErow, introduces no deny/dead-end path, no "not supported"/"unavailable" string, and flips no test to assert something is impossible. The only "you can't" prose is the README's note that an immediate retry does not clear a platform hold, which restates the spec's own description of the status rather than asserting a missing feature — and it is written so the retry-later path stays visible.Verification against what the ticket names
Every artifact the ticket points at, exercised read-only:
spec/openapi.yaml— read both 409s:hash_mismatchonPOST /api/v2/assets(line 117) andasset_in_useonDELETE /api/v2/assets/{id}(line 278), whose description is the source for "the body deliberately never says which hold applies".src/comfy_low/errors.py_BY_CODEandsrc/comfy_sdk/exceptions.py_BY_CODE— both confirmed to have lackedasset_in_use, both now carry it.409 asset_in_useon both layers — the stated acceptance criterion, covered bytest_an_enveloped_delete_conflict_is_typed_at_the_protocol_layer/..._at_the_sdk_layerplus three end-to-end tests through the realassets.delete()/Asset.delete()/ async paths.mainmerge. The two changes stayed independent as expected (fix(errors): drop 409 from the status->code fallback so a code-less 409 raises ComfyError #132 touches_CODE_BY_STATUS, this touches_BY_CODE), and together they close both halves of theasset_in_usestory — see Residual. Based onmain, not stacked.pyproject.toml— confirmed the version is the placeholder the publish workflow overwrites, which is why the changelog entry is under[Unreleased].Provenance
mainmerged in atc59e375):ruff check .: all checks passed;ruff format --check .: 57 files already formatted;mypy src: no issues in 21 source files;pytest: 982 passed, 9 skipped (skips are the network-gated integration tests, skipped onmaintoo);scripts/check_drift.py: models in sync, all 18 router error types covered, model-run URL as declared;scripts/check_public_repo_hygiene.py: no internal-only references.AssetInUsedocstring and thetest_error_mapping.pycomment now spell the routeDELETE /api/v2/assets/{id}(panel nit — the tree reserves bare/v2/...for the Router surface), andAssetInUsewas added tocomfy_sdk.exceptions.__all__, which the first pass missed while updating both packages'__all__; a guard test now asserts every class in_BY_CODEis in__all__.Residual
AClosed — #132 has merged and is in this branch. When this was written,409that carries no envelope code is still mistyped asHashMismatch, on delete as well as upload._CODE_BY_STATUSstill mapped409 → "hash_mismatch", so a bare409fromDELETE /api/v2/assets/{id}— one from an intermediary, or a surface answering the status without the envelope — reached the caller asHashMismatch. That row was left untouched here because removing it was the whole subject of the then-open #132, and changing it in two places at once would have conflicted.#132 has since merged and arrives in this branch with the
mainmerge: it dropped409from_CODE_BY_STATUSoutright, so a code-less409now decodes tohttp_409and surfaces as a plainComfyErrorcarrying the realhttp_status, the response's message and anyRetry-After. The two changes compose as intended —except AssetInUseis reliable for enveloped delete conflicts, and a code-less409no longer claims to be an upload failure instead. The cursor-review panel raised this same gap against this PR (Medium); it is resolved by the merge rather than by a change on this branch.Three of the twelve error codes the v2 spec names on a 4xx/5xx response still have no typed class at either layer, which is the same defect this ticket describes, one artifact over. I ran the same sweep over the remainder rather than only over the code I fixed: of the 12 codes
spec/openapi.yamlnames on an error response, 9 are now typed at both layers, and these 3 are not:too_many_streams(GET /jobs/{id}/events, line 579) — the closest sibling of this ticket. It reaches a caller as a bareComfyErrorand the correct handling (close a stream, or fall back to polling) is exactly the kind of decision a typed class should carry.deployment_not_ready(429 on deployment-scoped surfaces, line 447) — less acute, becauseclient.py's submit path already retries429 + Retry-Aftergenerically, so it is usually absorbed before a caller sees it. It is still indistinguishable fromqueue_fullby class.not_implemented(501 from the events endpoint, line 588) — mostly absorbed too:jobs.pycatcheshttp_status == 501and falls back to polling. A caller reaching a raw transport call still gets a bare error.None of these were in scope here and none are caused by this change; they are named because the sweep that sized this fix could see them.
Nothing was exercised against a live deployment. Verification is against the repo's stdlib stub server, which is the repo's own mandated approach (
AGENTS.md: the suite has no network dependency, driveserver.staterather than mocking httpx).tests/integration/test_gateway_e2e.pyis gated onCOMFY_BASE_URL+COMFY_API_KEYand skipped in this run, so no real409 asset_in_usefrom a real surface was observed — the wire shape is taken fromspec/openapi.yamlas the contract of record. If a deployment's delete conflict turns out not to carryerror.codein the envelope, the code-less gap above is what it would hit.Summary by CodeRabbit
AssetInUseerror for asset deletion requests refused because the platform still depends on the asset.