mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #205] feat(errors): map sso_required to Forbidden and carry organization_id #205
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#205
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/205
Author: @mattmillerai
Created: 10/9/2026
Status: 🔄 Open
Base:
main← Head:matt/be-19944-sso-required-forbidden📝 Commits (3)
5f4e920feat(errors): map sso_required to Forbidden and carry organization_id9f802c7fix(errors): gate and sanitise organization_id; keep Forbidden's bucket on sso_required1133ad5Merge remote-tracking branch 'origin/main' into matt/be-19944-sso-required-forbidden📊 Changes
8 files changed (+334 additions, -4 deletions)
View changed files
📝
CHANGELOG.md(+4 -0)📝
src/comfy_low/errors.py(+45 -0)📝
src/comfy_sdk/_errors.py(+12 -0)📝
src/comfy_sdk/exceptions.py(+19 -2)📝
src/comfy_sdk/router_exceptions.py(+12 -1)📝
tests/conftest.py(+32 -0)📝
tests/test_error_contract.py(+4 -1)➕
tests/test_sso_required.py(+206 -0)📄 Description
ELI5
Some Comfy Cloud organizations require their members to sign in through single sign-on. When a personal key is used on one of those accounts, the service refuses it and says which organization to sign in through. Before this change the SDK raised a generic error and dropped that information. Now it raises the usual "access denied" error, so existing error handling still catches it, and the error also carries the organization, so an app can send the user to the right sign-in page.
What changed
comfy_low.errors:ApiErrorgains anorganization_idkeyword and attribute._BY_CODE["sso_required"] = Forbidden.error_from_envelopereadserror.organization_idonly when the resolved code issso_required, and passes it through the newclean_organization_id. That keeps the value only if all of it is a bounded, id-shaped token ([A-Za-z0-9._:-]{1,200}). A missing, empty, non-string or malformed value becomesNone.comfy_sdk:ComfyErrorgainsorganization_idas a class-levelNonedefault, a constructor keyword and an attribute.RouterError.__init__accepts it, plus an optionalcode=, and passes both on toComfyError._BY_CODEmaps"sso_required"to the existingForbidden, which is theRouterErrorsubclass.to_sdk_errorpassesorganization_idthrough all three branches:QueueFull, theRouterErrorbranch (the onesso_requiredtakes) and the generic one. On theRouterErrorbranch,error_typeis the class's own bucket, sosso_requiredraisesForbiddenwitherror_type == "forbidden"andcode == "sso_required". For every other mapping the bucket already equals the code._STAMPED_ATTRIBUTESnow includesorganization_id.tests/test_error_contract.pyrequires every caller-readableComfyErrorattribute to be in this list. Without it,exc.organization_idon a stampedhttpx.ConnectErrororCancelledErrorwould raiseAttributeErrorinstead of returningNone._BASE_ATTRIBUTESin that test is extended to match.tests/conftest.py): a newsso_requiredknob, named after the TypeScript stub'sssoRequired. When it is set, every authenticated DELETE/GET/PUT/POST (not HEAD) answers403 {"error": {"code": "sso_required", "message": ..., "organization_id"?}}.organization_idis left out when the knob's value isNone.tests/test_sso_required.py:sso_requiredbecomesForbiddenwith the org. An absent, empty, blank, non-string, URL-delimited, control-character, bidi-override or overlongorganization_idgivesNone. The field is ignored onnot_found/queue_full/forbiddenenvelopes.to_sdk_errorbranch forwards the org,translating()keeps it when it stamps a key, anderror_typestays insideROUTER_ERROR_TYPES.require_authon: syncjobs.get, syncsubmit, and asyncjobs.get, with the org known and unknown.Judgment calls
SsoRequiredclass, on purpose.comfy_sdk'sForbiddenis a Router bucket class, and that surface is one class pererror_type, which the TypeScript SDK's parity snapshot checks. I ran the TS repo'spython-surface.mjsextractors (router classes and error types, status map, exported error classes) on this branch and onorigin/main: the output is identical.organization_idis read only off ansso_requiredenvelope. This makes the documented "Noneon every other code" true, and means an unrelated 404/429 body that happens to carry the field cannot lead a caller into starting SSO. (Changed in review; the first version read it off any envelope.)organization_idis checked as a whole token, not shortened to its valid prefix (unlikeclean_request_id) and not run throughclean_body_excerpt. Callers put it into a URL, and a shortened org id would start SSO for the wrong organization, so a value that does not match is treated as absent. Callers should still URL-encode it when building the SSO start URL.tests/test_errors.py/tests/test_exceptions.py, which do not exist in this repo, so the cases are in a new focused file instead.Provenance
maininto this branch)ruff check,ruff format --check,mypy srcall clean at the current headbuild-check, codegen drift and hygiene all pass_BY_CODEentry, or theRouterError→ComfyErrorhand-off, makestest_sso_required.pyfailorigin/maintests/test_sso_required.py, because the two file names the plan gave do not exist here. HEAD probes cannot carry the org (no body), and this is documented on the attribute rather than recovered.Residual
tests/integration/test_gateway_e2e.pyneeds a real key on an SSO-held account, and I did not run it. The real gateway'ssso_requiredbody shape is taken from the vendored contract and the stub, not observed directly.HEAD /api/v2/assets/by-hash/*with 403, the SDK raises a plainforbiddenwithorganization_id=None. This is now documented onApiError.organization_id/ComfyError.organization_id. The stub's HEAD handler has no auth check and does not model it.organization_idis not in theErrorEnvelopeschema in the vendoredspec/openapi.yamlonmain. It arrives with the next one-way spec sync, which this PR does not depend on:error_from_envelopeis hand-written and the generated models are untouched.Summary by CodeRabbit
sso_requirederrors are reported asForbiddenwhile retaining their error code.None.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.