mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #206] fix(events): raise a typed error on the terminal SSE error frame instead of reconnecting #207
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#207
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/206
Author: @mattmillerai
Created: 10/9/2026
Status: 🔄 Open
Base:
main← Head:matt/be-19942-sse-error-frame📝 Commits (2)
5cfb314fix(events): raise a typed error on the terminal SSE error frame instead of reconnecting5f9e8fdfix(events): leave an error frame with an unknown code to the poll fallback📊 Changes
10 files changed (+259 additions, -4 deletions)
View changed files
📝
CHANGELOG.md(+4 -0)📝
README.md(+8 -0)📝
src/comfy_low/__init__.py(+2 -0)📝
src/comfy_low/errors.py(+42 -0)📝
src/comfy_sdk/events.py(+3 -0)📝
src/comfy_sdk/exceptions.py(+15 -2)📝
src/comfy_sdk/jobs.py(+32 -2)📝
tests/conftest.py(+11 -0)📝
tests/test_error_mapping.py(+52 -0)📝
tests/test_events.py(+90 -0)📄 Description
Summary
The server ends a job's event stream with
event: errorand an error envelope (credential_expired,forbidden, orjob_not_found) when it stops streaming for a reason other than the job finishing.event_from_rawreturnedNonefor that frame, soJob.events()/AsyncJob.events()skipped it, saw the stream close without a terminal status, polledGET /jobs/{id}, slept 0.1 s and reconnected. Forjob_not_found, whileGET /jobs/{id}still answers, that loop never ends and keeps using up the concurrent-stream quota. For every code, the caller never learned why the stream ended.Change
comfy_low.errors.sse_error_from_frame(data)(also exported fromcomfy_low): for the three documented codes it reads the frame's envelope witherror_from_envelope, taking the status from the code (credential_expired→401,forbidden→403,job_not_found→404) and keepingcodeexactly as sent. It returnsNonefor any other code or for a malformed frame (noerror, or a non-dict or non-string code). Such a frame isn't known to end the stream for good (a later server might drain or cap a stream's lifetime), so it is left to the existing poll-and-reconnect path instead of being raised as an untypedHTTP 0error.credential_expired→Unauthorizedis now mapped in bothcomfy_low.errors._BY_CODEandcomfy_sdk.exceptions._BY_CODE. The caller getscomfy_sdk.exceptions.Unauthorizedwith.code == "credential_expired".to_sdk_errorkeeps.error_type == "unauthorized"(the class's Router bucket) for this alias, soerror_typenever names a value outside the closed Router set.events()bodies check forraw.event == "error"beforeevent_from_raw. For a known code they raise inside the existingtry, and for any other code they skip the frame. The existingexcept ApiErrorsends it throughto_sdk_errorbecause the status is never 501. There is norefresh()and no sleep. Both docstrings are updated.events.py: a comment oncase _:explains whyerroris not anEventsubclass.Unreleased → Fixedentry, plus a short paragraph in the README's Live progress section saying a server-ended stream raises instead of reconnecting.Judgment calls
sse_error_from_frameunit test intests/test_errors.py. That file does not exist, so the tests went into the existingtests/test_error_mapping.py, which already coverserror_from_envelope.state.poll_countisstate.job_poll_countin the stub. The tests get the job fromclient.submit(...), which does not poll, and record the poll count after that.sse_error_from_framefromcomfy_low, next toerror_from_envelope. The ticket did not ask for this.Residual
spec/openapi.yamldoes not list anerrorevent undergetJobEvents'x-sse-events. This PR handles the frame because the server already sends it, but the contract does not document it yet. The fix belongs in the canonical spec, followed by a re-sync. The vendored spec is not edited here (one-way copy).{"error": {"code", "message"}}and the three codes come from the server-side description of the change. They are exercised only through the stdlib test stub.tests/integration/test_gateway_e2e.pywas skipped as usual (no credentials set) and does not cover this path anyway.async forleaves the low-level async generator for the GC to finalize instead of explicitly callingaclose(). The existing terminal-statusreturnin the same loop already behaves this way, so this is not a regression, but it could be its own small cleanup.Provenance
src: cleanraw.event == "error"check disabled, and the unknown-code fallback tests fail when every error frame is raisedbuild-checksse_error_from_frametests are intests/test_error_mapping.py, nottests/test_errors.py, which does not exist. Unlike the original ticket, only the three documented codes raise, and any othererrorframe falls back to polling (review feedback).🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.