mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #174] [MERGED] fix(errors): sanitise the string detail and error.message forms #174
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#174
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/174
Author: @mattmillerai
Created: 9/19/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15951-sanitise-string-detail📝 Commits (7)
2096320fix(errors): sanitise and unify the Router detail[] validation summary9525758merge main, and drop the detail[] reroute the panel flagged4c451b3fix(errors): sanitise the stringdetailanderror.messageformsc491678Merge branch 'main' into matt/be-15951-sanitise-string-detaile87d842fix(errors): do not charge the summary budget for detail entries that sanitise awaybafc17ctest(router-spec): pin credits_used to the contract header the spec now declares40332dcMerge remote-tracking branch 'origin/main' into matt/be-15951-sanitise-string-detail📊 Changes
7 files changed (+387 additions, -31 deletions)
View changed files
📝
CHANGELOG.md(+16 -0)📝
src/comfy_low/errors.py(+87 -27)📝
src/comfy_sdk/router_exceptions.py(+21 -4)📝
tests/conftest.py(+8 -0)📝
tests/test_error_mapping.py(+114 -0)📝
tests/test_models_run.py(+36 -0)📝
tests/test_router_exceptions.py(+105 -0)📄 Description
STACKED — merging lands on
matt/be-15633-run-422-sanitise-detail(owned by @mattmillerai, PR #167), NOTmain. Do not merge this before #167. Its hunks inerror_from_envelopeanderror_from_responsesit directly against the lines this PR changes, so basing onmainwould have conflicted; GitHub will retarget this PR tomainwhen #167 merges and its branch is deleted.ELI-5
When a request fails, the SDK builds an exception out of whatever the server said. Some of that text was already being cleaned up before it was shown — control characters turned into spaces, everything squashed onto one line, cut off at 256 characters — because an error message is printed into a terminal and a log, and a server (or a proxy, or a model provider standing behind one) can put anything at all in it. But only some of it. Three of the ways the same human-readable string arrives were still passed through untouched, so a
detailfull of terminal escape codes, a right-to-left override and ten thousandxes landed in your traceback exactly as sent. Now all of them get the same clean-up.What changed
Four builders handed a server-, proxy- or provider-supplied string to
str(exc)with at most a.strip():src/comfy_low/errors.pyerror_from_envelopeerror.messagesrc/comfy_low/errors.pyerror_from_envelopedetailsrc/comfy_sdk/router_exceptions.pyerror_from_responsedetail = raw_detail or Nonesrc/comfy_sdk/router_exceptions.pyerror_from_completiondetail = raw_detail or NoneAll four now go through
comfy_low.errors.clean_body_excerpt— the same function thedetail[]array summary (#167) and every body excerpt already use:Cc/Cf/Co/Cscharacters replaced with spaces, whitespace collapsed to a single line, and a 256-character cap, with only a bounded head of the input examined so the cost of describing a body does not scale with the body.Deliberately not changed:
_clean—error.code, the Router bucket (error_type, header and body), a cancel refusal'sstatus. Those are wire tokens that get compared and branched on;clean_body_excerpt's whitespace collapse and 256-char cap would change a value rather than make it printable.ValidationErrorDetail.msgandRouterError.errors/ApiError.validation_errors— those are data, raw by design. See Residual.clean_body_excerptis imported from the module (from comfy_low.errors import ...) exactly asclean_request_idandsummarise_detailalready are. No new public re-export.Docs: the
error_from_envelopedocstring now states that bothdetailspellings anderror.messageare reduced;_clean's docstring says what is left to it and why; theRouterError.detailattribute comment says the value is sanitised and capped at 256 when it came off the wire (and that constructing the class by hand bypasses that); one Fixed entry under## [Unreleased].Behaviour change
clean_body_excerptreturnsNonefor a non-string or a blank input, exactly as_cleandid, so thedetail[]list guard, theHTTP <status>fallback and the message-foundbody_excerptgate all behave as before. One difference is real and intended: a whitespace-onlydetailnow reads as absent on both router builders and falls to the existing fallback (HTTP 502, orthe request completed with error_type '<bucket>') instead of surfacing as a blank description. That matches whaterror_from_envelopealready did for the same input. Adetailmade entirely of unprintable characters ("\x00\x00") reduces to nothing the same way, and the body excerpt is then kept rather than dropped — strictly more information than before.How this was tested
Red-then-green, verified by reverting only the two source files and re-running: 10 of the 14 new tests fail without the change (the other 4 assert pre-existing invariants the change must not break).
tests/test_error_mapping.py—error_from_envelopeparametrised over the Router stringdetailand the v2error.message, withHOSTILE = "\x1b[31mBAD\x1b[0m\nreversed\x00" + "x" * 10_000: no newline, no\x1b, no, every character outside{Cc, Cf, Co, Cs},len(err.message) == _BODY_EXCERPT_LIMIT,err.body_excerpt is None. Plus: the words survive the reduction (no healthy\x00upstream\n→no healthy upstream), a blank/zero-width-onlydetailfalls back toHTTP 502, and a hostilemessagedoes not disturberror.code.tests/test_router_exceptions.py— the same assertions parametrised overerror_from_response(502, …)anderror_from_completion(…, request_id="req_1"), on.detailand onstr(exc); a" "detail yielding each builder's fallback; and.errors[0].msgconfirmed still raw while.detailis bounded.tests/test_models_run.py— one end-to-end case through the stubbed server, proving the wholemodels.run→transport.parse_or_raise→error_from_envelope→to_sdk_errorchain (the only path the SDK itself takes to a stringdetail;error_from_responsehas no in-tree caller). Needed one new knob on the fake server,ServerState.model_run_error_detail, so a test can choose the failure's human-readable string.Every pre-existing message-equality assertion passes unchanged — no expected string was edited and the sanitiser was not weakened.
Provenance
mainand the two review-round commits:uv run --extra dev pytest -q: 1029 passed, 10 skipped;ruff check .: all checks passed;ruff format --check .: 57 files already formatted;mypy src: no issues in 21 source files. All 12 PR checks green, includingTest (py3.10/3.11/3.12/3.13),comfy_low codegen driftandpublic-repo-hygiene. Red-then-green confirmed twice: reverting the two original source files gives 10 failed, 5 passed; reverting thesummarise_detailbudget fix alone fails its new regression test.summarise_detailcharged its length budget fordetail[]entries measured before sanitisation, so one entry of control characters could exhaust the budget, reduce to nothing, and cost the caller every readable entry behind it — a bareHTTP 422. Reported by CodeRabbit, reproduced end to end, fixed with a regression test.mainwas already red atc0c4a33on its own, independently of this branch: #166 added thecredits_usedlift while its header was undeclared and #177 synced a spec that declares it, tripping the deliberate tripwire intest_router_spec_contract.py. Mergingmainbrings that failure here, so it is reconciled the way the tripwire's own message prescribes — the lift is moved into_CONTRACT_HEADER_LIFTS, which strengthens the pin rather than removing it. No base-branch assertion was deleted or weakened.Residual
Not fixed here, and worth its own look.
ValidationErrorDetail.msgandRouterError.errors/ApiError.validation_errorsstill carry the server's bytes verbatim, and nothing bounds them. This is deliberate — they are typed data a caller branches on, not display text, and the task explicitly scoped them out — but it is a real remaining exposure on the same class of input this PR is about: a caller who printsexc.errors[0].msgor logsexc.validation_errorsdirectly gets the unreduced string, including escape sequences and unbounded length.summarise_detailprotects only the summary it builds. If that is to be closed, the answer is probably a documented display accessor rather than sanitising the data, since sanitising it would destroy the field's value as data.RouterError(detail=...)constructed by hand is not sanitised. The reduction lives in the three builders, not in__init__, so anything outside this module that constructs the class directly with server text is still exposed. I documented that on the attribute rather than moving the reduction into the constructor (which would also reduce the SDK's own status-derived fallback sentences). No in-tree caller does this today.detailis a silent behaviour change for anyone who was reading it. It is in the changelog, but it is the one thing in this PR a downstream assertion could notice.Not exercised.
AGENTS.md). The claim that a real intermediary sends such adetailis inference from the shapes the code already handles, not something I observed on live traffic.not supported/unavailable/STOPstring, no throw or deny dead-end, and flips no test to assert one. The only reachability change is a whitespace-onlydetailrouting to an existing fallback that already produced a useful message.src/comfy_low/andsrc/comfy_sdk/for every read of a server-supplied string that reaches a user-visible message. Four were display text and all four are fixed here; zero unsanitised display-text sites remain. Nine_clean-only reads remain and are all code tokens kept on purpose (error.code, the bucket in four places, a cancelstatus, the twoerror_type/codereads inretry.pyandmodel_requests.py). Every other.strip()in the two packages validates caller input, not wire text.Summary by CodeRabbit
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.