Repository navigation
fix(events): raise a typed error on the terminal SSE error frame instead of reconnecting - #206
Open
mattmillerai wants to merge 2 commits into
Open
mattmillerai wants to merge 2 commits into
mattmillerai wants to merge 2 commits into
Conversation
…ead of reconnecting
The server ends a job's event stream with `event: error` and an error
envelope (credential_expired / forbidden / job_not_found) when it stops
streaming for a reason other than the job finishing. event_from_raw
dropped the frame, so Job.events() and AsyncJob.events() saw the stream
close without a terminal status, polled, and reconnected - for
job_not_found while GET /jobs/{id} still answers, forever.
Both iterators now raise the frame via comfy_low.sse_error_from_frame,
which maps it to Unauthorized / Forbidden / NotFound (code kept
verbatim). credential_expired maps to Unauthorized in both code tables.
|
Warning Review limit reached
This review includes 10 billable files and costs up to $2.50.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 37 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Your 131 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (10)
Comment |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 4 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 2 |
| ⚪ Nit | 1 |
Panel: 5/6 reviewers contributed findings.
Reviewers that did not contribute: kimi-k3-max:edge-case (error)
…llback Raise only for the documented terminal frame codes (credential_expired, forbidden, job_not_found). Any other code, or a frame with no readable envelope, is treated as the stream ending early, so a future graceful drain or lifetime cap does not crash a still-running job's iteration with an untyped HTTP 0 error. to_sdk_error keeps the Router bucket as error_type for the credential_expired alias; the wire spelling stays on .code.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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