mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #141] [MERGED] feat: name the status on an unrecognised error and keep the body that explains it #141
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#141
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/141
Author: @mattmillerai
Created: 9/10/2026
Status: ✅ Merged
Merged: 9/10/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-11430-http-status-code-body-excerpt📝 Commits (4)
827574efeat: name the status on an unrecognised error and keep the body that explains itd81da24fix: address bot review commentsde0c69bMerge origin/main into matt/be-11430-http-status-code-body-excerpta707792docs(changelog): keep this branch's entries under Unreleased after the 0.1.9 cut on main📊 Changes
6 files changed (+447 additions, -13 deletions)
View changed files
📝
CHANGELOG.md(+35 -0)📝
README.md(+9 -0)📝
src/comfy_low/errors.py(+122 -7)📝
src/comfy_low/transport.py(+34 -1)📝
src/comfy_sdk/exceptions.py(+8 -3)📝
tests/test_error_mapping.py(+239 -2)📄 Description
ELI-5
When something in front of the deployment answers instead of the service — a load balancer with no healthy backend — you got
ApiError(code="error", message="HTTP 503")."error"is the class default any hand-built exception carries, so it told you nothing, and the one line that did name the cause (no healthy upstream) was thrown away with the response. Now you getcode="http_503"andHTTP 503: no healthy upstream.What changed
codefor an unrecognised response is nowhttp_<status>instead of"error". This is the last fallback inerror_from_envelope, reached only after the envelope's ownerror.code, Router'sX-Comfy-Error-Typebucket, and_CODE_BY_STATUShave all declined._CODE_BY_STATUSis untouched — a bare401with no body still maps toUnauthorized, and every documented code is unaffected."error"was indistinguishable from "nobody set a code", on the one class of failure where the SDK has nothing else to say; the status is the single fact such a response does carry. Thehttp_prefix is deliberate: it reads as no service verdict was reached — something in front of the API answered, and it cannot collide with a wirecodeor a Router bucket, none of which are spelled that way.ApiError.body_excerpt(keyword-only, defaults toNone) keeps the text that response served. Whitespace-collapsed to one line, control characters stripped, capped at 256 characters. It is set only where the body was not the error envelope — where the response has already stated its cause, a second copy of the same text helps nobody — and__str__shows it only beside the syntheticHTTP <status>message, never spliced into a message the server actually sent.Retry behaviour is unchanged.
RetryPolicykeys on the status plus the two collectable buckets (deadline_exceededon a504,concurrency_limit_exceededon a409);http_<status>is neither, so such a 5xx remains "a completed 5xx that named no pace" and is not auto-retried. Asserted directly intest_an_unrecognised_status_is_still_not_retried_on_its_own.http_<status>survives the layer boundary unchanged.to_sdk_errorre-types a preserved Router bucket into its typed class;http_503is not one, so it arrives as a plainComfyErrorstill carryingcode="http_503". No code change was needed for this — it is pinned by a new test so a later widening of the re-typing table cannot silently swallow it.Judgment calls
resp.text"; the implementation caps at 256 characters of the whitespace-collapsed text, which is what the plan's own test criterion ("a 2 KB body is truncated to 256 chars") describes, and slicing raw bytes would have meant either splitting a multi-byte UTF-8 sequence or discarding the response's declared charset.clean_request_idbounds the request id for exactly that reason; an escape sequence in an error page must not repaint the terminal reading it.parse_or_raisealso raisescode="invalid_response"for a success status whose body will not decode — a proxy interstitial served as a200. That has the identical defect (the message says the body would not decode; only the body says what answered), so it carries the excerpt too. It is one argument, purely additive, and__str__is unaffected there because that message is not the synthetic one.resp.textis read behind a guard. It raisesResponseNotReadon a streaming response nothing has read yet — the same case the existingresp.json()already degrades on — and a failure while composing an error message must not replace the error it was describing. Covered bytest_an_unread_streaming_body_degrades_instead_of_raising.exc.code == "error"to detect an unrecognised 5xx would need to matchexc.code.startswith("http_")instead. Filed under Changed in the CHANGELOG rather than as a breaking change:"error"was the class default and was documented in the code as the meaningless one.Verification
uv run --extra dev ruff check .— all checks passeduv run --extra dev ruff format --check .— 51 files already formatteduv run --extra dev mypy src— no issues, 19 source filesuv run --extra dev pytest -q— 739 passed, 4 skipped, 0 failedpython3 scripts/check_public_repo_hygiene.py— no internal-only references found"error"for a bare 5xx (grepovertests/for== "error"returns onlytest_models_run_retry.py:1003, which builds anApiErrorby hand and therefore reads the untouched class default).Residual
ApiError.body_excerptdoes not reach thecomfy_sdksurface.translating()converts every protocolApiErrorinto aComfyErrorviato_sdk_error, andComfyErrorhas nobody_excerpt, so an integrator catchingComfyErrorfromclient.run()/client.models.run()seescode="http_503"but not the text that names the cause —str(exc)there is still the bareHTTP 503. The code half of the fix crosses the boundary; the body half does not. This was left out deliberately: the plan namedcomfy_low/errors.py,comfy_low/transport.pyand the code carry-through only, andComfyError's attribute surface is an explicitly-reviewed published contract (_BASE_ATTRIBUTESintests/test_error_contract.py, spelled out precisely so that adding one is a deliberate edit). Completing it means: addbody_excerpttoComfyError.__init__and its class defaults, forward it into_sdk_error, add it to_BASE_ATTRIBUTES, and decide whetherComfyError.__str__should mirror theHTTP <status>gating. The README bullet added here says explicitly that the text lives on the protocol-level exception, so the docs are not overclaiming in the meantime.comfy_sdk.router_exceptions.error_from_response— a public, exported helper with no caller insidesrc/today — degrades a bare load-balancer503toRouterError(detail="HTTP 503")witherror_type=Noneand the body dropped.503is deliberately absent from its_ERROR_TYPE_BY_STATUS(a gateway's503is not the router'sservice_unavailable), and that decision is correct and unchanged here; what is missing is the same body-excerpt keep-and-display on that path. The ticket scoped itself to the envelope fallback and says so.http_<status>be read as "no server verdict" is stated against a harness that lives in another repository, at a path this worker cannot read from its sandbox. I verified the SDK-side half of that contract instead — thatto_sdk_errorcarrieshttp_503through unchanged and that no re-typing or flattening happens on the way out — but I did not run, read, or otherwise confirm the harness's ownstartswith("http_")check, nor that its line reference is still current. If that check has moved or changed shape, this prefix may not be the one it is looking for.no healthy upstream/upstream connect error or disconnect/reset before headersbodies come from the originating investigation, not from a capture I took. The tests drive realhttpx.Responseobjects carrying those exact bodies through the real raise site, which exercises the code path end to end, but the wire shape itself (in particular the assumption that such a response carries noX-Comfy-Error-Type) is inherited rather than re-observed.Provenance
ruff check .: all checks passed;ruff format --check .: 51 files already formatted;mypy src: no issues in 19 source files;pytest -q: 739 passed, 4 skipped, 0 failed;scripts/check_public_repo_hygiene.py: cleanbody_excerptis not propagated to thecomfy_sdkexception surface (see Residual)Summary by CodeRabbit
Bug Fixes
http_502.Documentation
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.