Skip to content

fix!: answer FastAPI session guard refusals under error - #39

Merged
Bccorb merged 1 commit into
mainfrom
fix/fastapi-guard-error-body
Nov 6, 2026
Merged

Bccorb merged 1 commit into
mainfrom
fix/fastapi-guard-error-body

Conversation

@Bccorb

@Bccorb Bccorb commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

FastAPI's RequireUser answered its refusals as {"detail": ...}, while Django, the permission guard and every other adapter answer {"error": ...}. This moves all of them to {"error": ...}. The difference was recorded by a test in #38, which this replaces.

Behaviour change for adopters

This changes a response body adopters may read.

Status Before After
401 {"detail": "unauthenticated"} {"error": "unauthenticated"}
403 {"detail": "insufficient_role"} {"error": "insufficient_role"}
403 {"detail": "enrollment_required"} {"error": "enrollment_required"}
403 {"detail": "cross_site_request_blocked"} {"error": "cross_site_request_blocked"}
  • A client that reads body.detail on these responses must read body.error. Status codes and error strings do not change, and Django is unaffected.
  • These refusals no longer reach an application's own HTTPException handler, because the guard's handler for its more specific class is found first. To change them, register a handler for GuardHTTPException. Permission refusals already worked this way.
  • Code that catches the exception around a guard still sees exc.status_code and exc.detail, which holds the error string.

Also changed

PermissionHTTPException, added in #38, is now GuardHTTPException, since it now covers every guard refusal. #38 is not in a release yet (v0.6.0 is still open as #36), so no released name changes. If #36 merges first, the rename becomes breaking and the commit message needs rewording.

Release note

The commit is fix!: with a BREAKING CHANGE: footer, so release-please should bump the minor version and list this under breaking changes. On a squash merge, keep the fix!: title and the footer in the commit message.

Testing

  • uv run ruff check ., uv run ruff format --check ., uv run mypy: pass
  • uv run pytest: 1084 passed
  • seamless verify --adapter-url with SEAMLESS_VERIFY_CONTRACTS=content-type-gate, against seamless-cli and seamless-auth-api main: passed for the FastAPI and Django reference apps

The adapter specs in the conformance suite do not assert these error bodies, so that run shows nothing else regressed. The new shape is covered by the unit tests, including a mounted app and an application handler.

Not in this PR

  • Verify-only refusals (BearerHTTPException) still sit under detail unless the adopter calls add_bearer_error_handler.
  • build/python-adapter.mdx in seamless-auth-docs still says the package has no role guard.

RequireUser answered its 401, 403 and recovery refusals as {"detail": ...},
while Django, the permission guard and every other adapter answer
{"error": ...}. All of them now answer {"error": ...}, with the same status
codes and the same error strings. exc.detail still holds the error string.

PermissionHTTPException, which never shipped in a release, is now
GuardHTTPException, since it covers every guard refusal.

BREAKING CHANGE: FastAPI RequireUser refusals (unauthenticated,
insufficient_role, enrollment_required, cross_site_request_blocked) answer
{"error": "<code>"} instead of {"detail": "<code>"}. A client that reads
body.detail must read body.error. They also no longer reach an application's
own HTTPException handler. To change them, register a handler for
GuardHTTPException.
@Bccorb
Bccorb merged commit 76739c1 into main Nov 6, 2026
7 checks passed
@github-actions github-actions Bot mentioned this pull request Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant