mirror of
https://github.com/Comfy-Org/comfy-typescript-sdk.git
synced 2026-11-04 23:47:27 +00:00
[PR #96] [MERGED] fix!: align the Router status fallback and base error_type with the Python SDK #98
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
bug
cursor-review-pin
effort:medium
effort:small
effort:trivial
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-typescript-sdk#98
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-typescript-sdk/pull/96
Author: @mattmillerai
Created: 8/27/2026
Status: ✅ Merged
Merged: 8/27/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-9820-router-error-fallback-parity📝 Commits (1)
6cb4960fix!: align the Router status fallback and base error_type with the Python SDK📊 Changes
8 files changed (+476 additions, -63 deletions)
View changed files
📝
.github/workflows/ci.yml(+81 -1)📝
CHANGELOG.md(+23 -0)📝
README.md(+12 -0)📝
parity/python-surface.json(+26 -3)📝
scripts/python-surface.mjs(+74 -8)📝
src/sdk/routerErrors.test.ts(+74 -9)📝
src/sdk/routerErrors.ts(+64 -20)📝
src/sdk/surface-parity.test.ts(+122 -22)📄 Description
ELI-5
When the Comfy Router says no, it tells you why in a header. Sometimes a proxy or load balancer answers instead and there is no header — then the SDK guesses the reason from the HTTP status code. It was guessing on
400and422, where a guess is not safe: a400means either "your input was invalid, fix it and try again" or "the content policy refused this, and it will refuse it again forever". Calling thatinvalid_inputsends a caller round a retry loop that can never succeed. The Python SDK already refuses to guess there. This makes the TypeScript SDK refuse too, and teaches the cross-SDK check to compare the two guess-tables so they cannot drift apart again quietly.What changed
1.
400and422are dropped from the status fallback table (src/sdk/routerErrors.ts). A header-less response on either status now raises the baseRouterErrorwithhttpStatusintact anderrorTypeempty, which is what the Python SDK already did. The source comment carries the reasoning per status.2. The trailing
|| "internal_error"on the resolution goes with them. This is required for (1) rather than incidental to it: leaving it would have turned a header-less400/422intoInternalError, not into the baseRouterErrorthe alignment calls for. It also means500and503without a header now raise the base class instead ofInternalError— the same behaviour the Python SDK has for those statuses, and the reason is the same one:internal_erroris a real member of the closed set, so it cannot also serve as "we could not tell".3.
RouterError.errorTypedefaults to""on the base class, matching Python'serror_type = "". Every subclass still declares and reports its own bucket; only the base changed.4. The parity check compares behaviour, not just names.
scripts/python-surface.mjsnow records the Python fallback table and the Python base class default inparity/python-surface.json, andsurface-parity.test.tscompares both. The TypeScript side of the fallback comparison is derived by callingtoRouterErroracross every status100–599rather than by reading its table — the table would have looked identical to Python's while the trailing|| "internal_error"silently changed the answer, so the behaviour is what gets asserted. The base class is no longer excluded from theerror_typecomparison.5.
sdk-paritygains a dailyschedule:trigger (.github/workflows/ci.yml,17 7 * * *). It is the only job whose input lives in another repository; the other three jobs are gated off scheduled runs withif: github.event_name != 'schedule'. A scheduled failure has no PR to redden, so it opens one sticky issue, comments on it on each subsequent failure rather than opening more, and closes it on the first run that passes again.github.event_nameis added to theconcurrencygroup so a cron run and a push to main — which share a ref — cannot cancel each other.6. Snapshot refresh, and one allowlist deletion. The Python SDK has shipped
deadline_exceeded,not_enabled,service_unavailableandrate_limitedsince the snapshot was last taken, sosdk-parityis already failing onmain— reproduced locally before any of this branch's changes (node scripts/sync-python-surface.mjs→parity/python-surface.json is stale). That is the detection lag item 5 is about, arriving on its own. The refresh is forced anyway by (4), which changes the manifest schema, and once refreshed therouter-buckets-ahead-of-pythonrot guard fires — which is that entry's own instruction ("delete this entry rather than grow it"), so the entry is deleted. All fifteen classes are now compared on both sides with no filter.Why the premise holds — checked against the artifacts, not assumed
The change makes the SDK say less about a class of responses, so the claim that the information genuinely is not there was checked rather than argued:
400really is ambiguous. The server mapsinvalid_inputandcontent_policy_violationonto the same400(read fromComfy-Org/cloud's Router error responder onmain, whose own comment records the choice and why the422is reserved for the per-field shape). Nothing downstream of the header can tell those two apart, and they differ in whether a retry can ever succeed.422really carries its bucket only on the header. The vendored contract in this repo says so directly —spec/router-openapi.yaml:490: "on the422it is the ONLY machine-readable bucket, because that body is the fal/FastAPIdetail[]shape and has noerror_typefield of its own" (also:371,:412).x-comfy-error-types(spec/router-openapi.yaml:189) declaresvalue,tierandmeaningper bucket and no status at all;400,401,403,404and503all share one genericRouterRequestErrorresponse whose body carries the full closed set.internal_erroris caller advice, not an SDK rewrite.spec/router-openapi.yaml:222: it "is also the value a client should treat any UNRECOGNIZED bucket as". The SDK preserving what the server actually said, and saying nothing where the server said nothing, is what lets a caller apply that advice.422still resolves toInvalidInput(withdetail[]intact) and toContentPolicyViolation, and that a body-carriederror_typeon a400still resolves.Blast radius
toRouterErroris not oncomfy.models.run's path — that surface buildsComfyErrorfrom its ownerrorFromResponse, untouched here. The retry policy reads the rawX-Comfy-Error-Typeheader and only forstatus >= 500, so it does not consult this table at all and its behaviour is unchanged.No published version is affected.
routerErrorshas never shipped: it is absent fromv0.1.7, the latest tag and the latest version on npm (verified withgit cat-file -e v0.1.7:src/sdk/routerErrors.ts→ does not exist, andnpm view @comfyorg/sdk versions). This is why the change is landed directly rather than behind a flag — a flag gates a behaviour change for existing consumers, and there are none; adding one here would ship a permanent fork of the error path to protect callers who cannot exist, and would itself become the divergence from the Python SDK that this change removes. Post-release the same edit would need one.The commit is marked
fix!because it is a behaviour change on a public symbol, even though the symbol is unreleased.Verification
Every CI step run locally and green:
oxlint,oxfmt --check,tsc --noEmit,check-spec-drift,check-public-repo-hygiene,vitest run(401 tests, 22 files),pnpm build, andnode scripts/sync-python-surface.mjs(nowOK, against the refreshed snapshot).The three new guards were falsified before being trusted — each was checked by reintroducing the defect and confirming it fails with an actionable message:
400: "invalid_input"→header-less HTTP 400: the TypeScript SDK raises \invalid_input` but the Python SDK raises the base RouterError (no bucket)`"internal_error"→routerErrors.RouterError: error_type is "internal_error" in the TypeScript SDK but "" in the Python SDK, and only that test plus the by-name one fail (the fallback derivation keys off the base default rather than a hardcoded""precisely so a regression there fails one test rather than three)_ERROR_TYPE_BY_STATUSguard rejects both an absent table and an empty one, tested on source it cannot read — the file's existing rule that a broken extraction must never read as agreementResidual
Not fixed here, actionable on its own:
schedule:workflows only from the default branch's copy, so no run on this branch executes either new step. The YAML parses and the job graph is as intended (verified by parsing the workflow and checking each job'sif:), and thegh issue list ... --jq '[.[] | select(.title == env.TITLE)] | .[0].number // empty'lookup was run read-only against this repo and returns empty as expected — but the create/comment/close paths themselves are unrun. First scheduled run after merge is the real test, and it will almost certainly fail and file the issue, since the check is red onmaintoday for the four-bucket drift; that first issue should auto-close on the run after this branch's refreshed snapshot lands.sdk-parityshould be a required check is a repo-owner decision and is deliberately not made here. As configured, Python-side drift reddens unrelated PRs in this repo until the snapshot is refreshed. That is the intended loudness and also exactly what gets parity checks switched off. The daily run now surfaces the same drift without a PR, which weakens the case for making it required — a maintainer should decide and record it.Comfy,Workflow,Job,Asset,Output): 24 of 39 root exports and 6 of 18routerErrorsmembers carry no name-parity assertion. Untouched here; worth its own change.router-spec-coverage.test.tsagainst the vendoredspec/router-openapi.yaml; the durable fix is generating the error classes from the spec.src/comfy_sdk/router_exceptions.pywas read (over public HTTPS) as the reference and deliberately not changed.routerErrorsnamespace has no### Addedentry inCHANGELOG.md's[Unreleased]section — a pre-existing gap this branch did not create. The### Changedentry added here therefore describes a behaviour change to classes the changelog has not yet introduced. Worth one entry before the next release.routerErrorClassesAheadOfPythonis now an empty mechanism. No asymmetry declares a lead, so its rot-guard test passes vacuously. The field and test are kept, documented as such, because the next bucket will land on one side first — but a reviewer who would rather delete unused machinery has a fair case.Provenance
|| "internal_error"catch-all (required for the stated 400/422 behaviour, and it also changes header-less 500/503 — matching Python), and deleting therouter-buckets-ahead-of-pythonallowlist entry (forced by its own rot guard once the snapshot was refreshed, which the schema change required).🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.