mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #130] [MERGED] fix(models): raise the original failure when a same-key retry is refused 422 #131
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
effort:medium
effort:small
effort:small
effort:trivial
priority:nice-to-have
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-python-sdk#131
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-python-sdk/pull/130
Author: @mattmillerai
Created: 9/5/2026
Status: ✅ Merged
Merged: 9/14/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-9949-run-raise-original-on-key-reuse📝 Commits (3)
04af794fix(models): raise the original failure when a same-key retry is refused 422999db21fix(models): only let a claim-capable failure suppress a key refusal (BE-9949)8dae76aMerge remote-tracking branch 'origin/main' into matt/be-9949-run-raise-original-on-key-reuse📊 Changes
6 files changed (+502 additions, -16 deletions)
View changed files
📝
CHANGELOG.md(+39 -0)📝
README.md(+14 -4)📝
src/comfy_sdk/exceptions.py(+24 -0)📝
src/comfy_sdk/models.py(+139 -4)📝
src/comfy_sdk/retry.py(+45 -5)📝
tests/test_models_run_retry.py(+241 -3)📄 Description
ELI-5
client.models.run()retries some failures by re-sending the request under the sameIdempotency-Key. Against a deployment that treats a key as single-use, that re-send comes back422 idempotency_key_reuse— and until now that refusal was the exception the caller got, throwing away the504/500that caused the retry in the first place. The refusal only says "you asked twice"; it says nothing about why the call failed. Nowrunraises the original failure and hangs the422off it as__cause__, so nothing is lost and the useful error is the one you see.What changed
Both retry loops in
src/comfy_sdk/models.py(sync and async, kept structurally identical) now remember the first retryable failure. If a later attempt is refused for key reuse, that first failure is raised with the refusal chained on:_is_key_reusematches the wire facts (http_status == 422andcode == "idempotency_key_reuse") and also accepts either layer's typedIdempotencyKeyReuseoutright, so the answer does not depend on which layer raised.translating(), which converts and stamps only the exception it is handed — anApiErrorchained on as__cause__would have reached the caller as a raw protocol type._as_sdk_errortherefore translates and stamps both halves before the raise, rather than changingtranslating()'s contract for its other 20 call sites.to_sdk_erroris already called directly this way injobs.pyandclient.py.Both halves carry the call's
Idempotency-Key, so the replay idiom still works off either.Only key reuse substitutes, and only after there has already been a retry. Nothing about which failures are retried changed:
_is_key_reusefires only on a422, whichRetryPolicy.should_retryalready answeredFalsefor, so short-circuitingdelay_before_retrycannot change any retry decision.Docs updated to match: the
retry_possibly_in_flightdocstring, the two module-docstring sentences inretry.pythat asserted the old behaviour ("a422that replaces the real error", "replaces the genuine 5xx"), theModels.run/AsyncModels.rundocstrings, three stale README passages, and a### Changedentry inCHANGELOG.mdflagging the behaviour change forexcept IdempotencyKeyReusecallers.Behaviour change
except IdempotencyKeyReusearoundmodels.runno longer catches the rejected-resend case. Catch the failure you care about (orComfyError) and inspectexc.__cause__. A key refusal on the first attempt — a caller passing a key the server already consumed — is unchanged and still raisesIdempotencyKeyReusedirectly; a test pins that.No capability is removed: the
422remains fully reachable (type, status, message, key, traceback) via__cause__, and three tests assert it is there rather than merely asserting it is gone.Tests
tests/test_models_run_retry.py, one updated and five added:test_the_default_collect_loop_against_a_non_collecting_deployment— it pinnedpytest.raises(IdempotencyKeyReuse), which was the bug. Now assertsComfyErrorwithhttp_status == 504,isinstance(exc.__cause__, IdempotencyKeyReuse), both halves carrying the same key, andmodel_run_count == 2. Theretry_collectable=Falsehalf is unchanged.AsyncComfy, which is what proves the second loop was edited too.500underretry_possibly_in_flight=Trueagainst a key-rejecting deployment —http_status == 500,__cause__isIdempotencyKeyReuse,model_run_count == 2, one key on the wire.500followed by a404still raises the404— last-wins survives for everything but key reuse.500→503→422surfaces the500.tests/conftest.pyneeded no change —ServerState.model_run_v2_key_rulealready answers a repeated key422 idempotency_key_reuse, andmodel_run_collects_after_deadlinealready models the non-collecting deployment.tests/test_sync_async_parity.pywas checked for a loop-source-shape assertion: it has none (it compares namespace/method names, kinds and signatures), so nothing there needed updating, and both loops are byte-for-byte identical apart fromawait/asyncio.sleep.Provenance
uv run --extra dev ruff check .clean;ruff format --check .51 files already formatted;mypy srcno issues in 19 source files;uv run --extra dev pytest -q723 passed / 4 skipped (the skips are the network-gatedtests/integration/test_gateway_e2e.py);python3 scripts/check_public_repo_hygiene.pyOK; plus a hand-run repro printing the chained traceback shown above.retry.pymodule-docstring sentence at lines 33/54/72; on currentmainthat sentence lives at line 84, and two further sentences asserting the old behaviour (inis_unknown_outcome_statusand theretry_possibly_in_flightfield doc) were updated alongside it. Three README passages also asserted the old behaviour and were corrected, which was not in the described scope but would otherwise have shipped stale.Residual
Comfy.submit/AsyncComfy.submitwere not changed and not exercised for this defect. They carry a structurally similar retry loop (src/comfy_sdk/client.py,_retry_delay) that re-sends under oneIdempotency-Keyand re-raises the last attempt's error. Today it retries only a429carryingRetry-After, which the v2 contract says releases the key, so a same-key resend there should not meet a422— the exposure is theoretical rather than demonstrated, and I did not build a repro for it. If a deployment is found that keeps a key claimed across a429,submithas exactly the bug this PR fixes inmodels.runand needs the same treatment._stampis imported across modules fromcomfy_sdk.exceptionsintocomfy_sdk.models(same package, private name). The alternative was wideningtranslating()to translate an explicit__cause__chain, which would have altered a helper used by ~20 call sites to serve one of them. Worth a reviewer's opinion; if the private import is unwanted, moving the translate-and-stamp pair intoexceptions.pyas a named public helper is a mechanical follow-up.504same-key-retry work (#99) and the exception key/request-id work (#97); both are merged, so this branches frommainand no stacking or rebase was needed, and no residual note could be added to either since neither is open.Summary by CodeRabbit
Bug Fixes
resend_refusedindicator for clearer diagnosis.New Features
Documentation
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.