mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #167] [MERGED] fix(errors): sanitise and unify the Router detail[] validation summary #167
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#167
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/167
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 9/20/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15633-run-422-sanitise-detail📝 Commits (2)
2096320fix(errors): sanitise and unify the Router detail[] validation summary9525758merge main, and drop the detail[] reroute the panel flagged📊 Changes
7 files changed (+377 additions, -54 deletions)
View changed files
📝
CHANGELOG.md(+9 -0)📝
src/comfy_low/errors.py(+106 -20)📝
src/comfy_sdk/exceptions.py(+13 -1)📝
src/comfy_sdk/router_exceptions.py(+27 -22)📝
tests/test_error_mapping.py(+195 -8)📝
tests/test_models_run.py(+16 -3)📝
tests/test_router_exceptions.py(+11 -0)📄 Description
ELI-5
When a model call is rejected for a bad field, the server sends back a little list of what went wrong. The SDK turns that list into the human-readable error message.
Two problems: (1) it pasted the server's text straight in without cleaning it, so a message containing a newline, a hidden control byte, or an ANSI escape could scribble on your terminal or flood a log line — and a big list could make the message enormous. (2) The message you got from
models.run()looked different from the one you got on the queued/submit()path for the exact same server response:run()dropped the field names, so twofield requirederrors collapsed to a uselessfield required; field required, while the queued path told you which two fields.This change cleans the text (same 256-char, single-line, no-control-chars treatment every other body-derived string already gets) and makes both paths build the message with one shared function, so one server response always reads the same way.
What changed
comfy_low.summarise_detail()renders eachdetail[]entry as<loc>: <msg>and runs the joined line throughclean_body_excerpt(unprintable-category reduction, whitespace collapse,_BODY_EXCERPT_LIMIT/256 cap). Botherror_from_envelope(awaitedrun()path) androuter_exceptions(queued path) now summarise the array through this one function, so a single wire body yields a single.detail. The now-redundant_summariseinrouter_exceptionsis removed._BODY_EXCERPT_LIMIT * _BODY_EXCERPT_WINDOWprefixclean_body_excerptactually reads, rather than rendering a server-controlled array in full and slicing 256 characters off the end — so describing a body costs the same whether the body is small or huge.locmembers that are not path segments are dropped, not coerced.locis typedtuple[str | int, ...]— field names and array indices — so a nested, server-controlled member is skipped instead of beingstr()-ed into a Python repr in the user-visible message. This applied in two places: the summariser, andValidationErrorDetail, whose_detail_fromwas storing the repr and whose.locationre-rendered it..locationnow delegates to the one shared renderer, so the summary and.errors[i].locationcannot disagree about what a field is called._cleandocstring corrected: the listdetailshape still reaches_cleanthrough themessage = _clean(raw_detail)fallback (a validation array that produced no summary), so itsisinstance(value, str)guard is load-bearing — it keeps a Python list repr out of the caller's message. The old docstring claimed the shape never reaches the function, which invited deleting the guard.Each item has a regression test; the run-vs-queued
.detailparity is asserted (assert excinfo.value.detail == queued.detail).Dropped after review: the
to_sdk_errorrerouteAn earlier revision made a
detail[]array select aRouterErroreven when the code collided with a v2 envelope bucket. That is removed, for two independent reasons:mainsolved it properly. #157 merged the two exception hierarchies, sounauthorized/forbidden/insufficient_creditsare oneRouterErrorsubclass each. For any response carrying real Router provenance,_class_foralready resolves to aRouterErrorsubclass — the only_BY_CODE/bucket overlaps are exactly those three, every other bucket hits_BY_ERROR_TYPE, and an unknown bucket falls back toRouterError— so the typed entries are forwarded with no help from this branch. Verified acrossinvalid_input, the three shared buckets and an unknown future bucket: all yield aRouterErrorwith.errorspopulated.detail[]is a body shape any server, proxy or gateway can send (a FastAPIRequestValidationErroris exactly it), so keying the class off it let an intermediary decide whichexcepta caller runs: a bucket-less 422 stopped raisingInvalidWorkflow, the electedRouterErrordroppedexc.details, and a v2-vocabulary code got stamped onto.error_type. It also contradicted the ruleerror_from_envelopealready documents, learned three times on live traffic — only a response that carries a bucket gets retyped.The tests that pinned the reroute now pin the opposite: a bucket-less array keeps its v2 class (
not isinstance(err, RouterError)), withdetailsintact and the per-field reasons still reaching the caller through the summary.Scope note / judgment call
The fix chose the "one summariser" option (over merely aligning the low-level format). A side effect is that the queued path's summary is now sanitised and bounded too — the same latent gap the awaited path had. This is deliberate and consistent, and it makes the run/queued
.detailparity exact for every body rather than only clean ones..errorsstill carries the raw typed entries; only the human-readable summary string is cleaned and field-qualified. This also changeserr.message/.detailon therun()path to include the field path (body.steps: too largerather than baretoo large) — the pre-existing per-field tests were updated to the corrected value, and there is a CHANGELOG entry for it.Testing
ruff check .— cleanruff format --check .— clean (57 files)mypy src— no issues (21 files)pytest— 987 passed, 9 skippedorigin/mainate4773c7(which landed #157) into the branch; the above was run on the merged tree.Provenance
9525758): ruff check + ruff format --check + mypy src all clean; pytest 987 passed, 9 skipped. The removal of theto_sdk_errorreroute was checked empirically — every Router-provenance bucket still yields aRouterErrorwith.errorspopulated, and a bucket-less body keepsInvalidWorkflowwith itsdetails.to_sdk_errorreroute described in the original revision is removed, not shipped — subsumed by #157 and flagged as a regression by 4 of 6 panel reviewers (see the section above). The sanitise-and-unify core is unchanged. One panel finding is deferred to a follow-up rather than fixed here (below).Residual
detail/error.messageforms are still passed through with only a.strip()(four sites acrosscomfy_low.errorsandrouter_exceptions), so a server-supplied string with control characters or ANSI escapes still reachesstr(exc)unbounded. Out of scope here — this PR is scoped to the array shape, and fixing the string forms changes.detailfor every non-validation error on both surfaces. Recorded for filing; the docstring no longer implies the string shape is covered.Post reviewstep failed twice and left them only in an expired GitHub Actions run artifact. That artifact is gone and cannot be fetched; it was not needed, as each finding was re-derived and verified against mergedmain.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
detailanderror.messageforms #174