Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ the fuller account of each version, including verification notes.

## [Unreleased]

### Added

- `ResultExpired` in `comfy_sdk.router_exceptions`, for the Router bucket `result_expired`
from the synced Router spec: a queued request that finished but whose result is no longer
stored, refused `410`. It is permanent for that request id, so do not retry the collect; it is
deliberately not `RequestNotFound`, which would say the id was never valid. Running the model
again means submitting a new request. Before this, the bucket arrived as a bare `RouterError`.

## [0.5.0] - 2026-10-10

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1105,13 +1105,13 @@ status on `.http_status`; keep an `except ComfyError` outside the clause above
if you need to handle those in the same place.

`RouterError` is exported from the package root because it is the handler most
callers write first. The nineteen per-bucket classes stay in
callers write first. The twenty per-bucket classes stay in
`comfy_sdk.router_exceptions` — `InvalidInput`, `ContentPolicyViolation`,
`ProviderError`, `ProviderTimeout`, `InsufficientCredits`, `ModelNotFound`,
`Unauthorized`, `Forbidden`, `ConcurrencyLimitExceeded`, `ClientDisconnected`,
`InternalError`, `DeadlineExceeded`, `NotEnabled`, `ServiceUnavailable`,
`RateLimited`, `Cancelled`, `QueueTimeout`, `RequestNotFound`,
`QueueBacklogFull` — one import path
`QueueBacklogFull`, `ResultExpired` — one import path
for the whole set rather than half of it here and half of it there. A bucket added to Router after your installed version
arrives as `RouterError` itself, with the raw value readable on `.error_type`.

Expand Down
103 changes: 86 additions & 17 deletions spec/router-openapi.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/comfy_sdk/model_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ def get(self, timeout: float | None = None) -> dict[str, Any] | BinaryResult:
Calling it on a request that has already completed is one status poll
and one fetch, so collecting a result twice — or from a second process
— costs no more than the first time.
That holds only while the server still stores the result: once it has
aged out, the fetch raises
:class:`~comfy_sdk.router_exceptions.ResultExpired`, which is permanent
for that request — submit a new one to run the model again.
"""
deadline = None if timeout is None else _now() + timeout
completion = _last(self.iter_events(timeout=timeout))
Expand Down
19 changes: 18 additions & 1 deletion src/comfy_sdk/router_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ class RequestNotFound(RouterError):
right-id / wrong-model URL the path shape refuses, and it is deliberately
indistinguishable from a request in another workspace, so a probe with a
guessed id learns nothing. A request that merely aged out of its retention
window is ``410``, not this.
window is ``410`` :class:`ResultExpired`, not this.
"""

error_type = "request_not_found"
Expand All @@ -562,6 +562,21 @@ class QueueBacklogFull(RouterError):
_spec_meaning_digest: str = "50745ff63044"


class ResultExpired(RouterError):
"""The request existed and finished, but its result is no longer stored, so
the collect has nothing to return.

Returned under ``410`` and permanent for that ``request_id``: re-reading
answers the same way, so do not retry it. It is deliberately not
:class:`RequestNotFound`, which would say the id was never valid and invite
re-submitting a generation that has already run. Running the model again
means submitting a new request.
"""

error_type = "result_expired"
_spec_meaning_digest: str = "986459c36e1d"


# -- cancel refusals ---------------------------------------------------------
#
# Deliberately OUTSIDE the closed set below, and carrying no
Expand Down Expand Up @@ -654,6 +669,7 @@ class AlreadyCompleted(CancelRefused):
QueueTimeout,
RequestNotFound,
QueueBacklogFull,
ResultExpired,
)

_BY_ERROR_TYPE: dict[str, type[RouterError]] = {cls.error_type: cls for cls in ROUTER_EXCEPTIONS}
Expand Down Expand Up @@ -940,6 +956,7 @@ def _detail_from(entry: Mapping[str, Any]) -> ValidationErrorDetail:
"QueueTimeout",
"RateLimited",
"RequestNotFound",
"ResultExpired",
"RouterError",
"ServiceUnavailable",
"Unauthorized",
Expand Down
1 change: 1 addition & 0 deletions tests/test_exception_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
(504, "queue_timeout"),
(404, "request_not_found"),
(429, "queue_backlog_full"),
(410, "result_expired"),
(418, "something_invented_later"),
]

Expand Down
2 changes: 2 additions & 0 deletions tests/test_router_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
QueueTimeout,
RateLimited,
RequestNotFound,
ResultExpired,
RouterError,
ServiceUnavailable,
Unauthorized,
Expand Down Expand Up @@ -74,6 +75,7 @@
("queue_timeout", 504, QueueTimeout),
("request_not_found", 404, RequestNotFound),
("queue_backlog_full", 429, QueueBacklogFull),
("result_expired", 410, ResultExpired),
]

# Deliberately not in the set this SDK version knows: a later milestone adds it,
Expand Down
Loading