Skip to content

fix: document request bodies and box query params in OpenAPI - #83

Merged
addisonkline merged 1 commit into
mainfrom
kline/v2-swagger-fix
Jun 29, 2026
Merged

fix: document request bodies and box query params in OpenAPI#83
addisonkline merged 1 commit into
mainfrom
kline/v2-swagger-fix

Conversation

@addisonkline

Copy link
Copy Markdown
Collaborator

Summary

POST/PATCH handlers took only request: Request and parsed their bodies by hand via validators.py, so FastAPI generated no requestBody schema — the bodies were absent from /docs, /openapi.json, and the committed spec/openapi.yaml. This promotes each request model to a typed handler parameter so FastAPI validates and documents all of them.

17 endpoints now document their JSON body (drafts, daemon, admin, lists, auth). The lone already-correct POST (/auth/token, via OAuth2PasswordRequestForm) was the model for the fix.

While here — folded in the same root cause on the query side: the GET /inbox|/outbox|/trash|/drafts endpoints now declare BoxFilterParams as a typed Query() param, so their limit/offset/sort_by/order filters are documented too.

Changes

  • Routers — declared typed body params across drafts, daemon, admin, lists, auth; declared BoxFilterParams query params on the box GETs. List policy guardrails preserved.
  • Optional body/auth/refresh and /auth/logout take the refresh-token body as Body(default=None) to preserve the cookie-only browser flow; _read_refresh_token is now sync and reads the parsed payload, keeping cookie-over-body precedence. Added endpoint descriptions noting the cookie wins.
  • validators.py — removed the now-unused body/query validators; kept the path-param validators.
  • scripts/generate_openapi.py — added the missing MAIL_REFRESH_TOKEN_EXPIRE_DAYS placeholder (latent bug from the refresh-token merge) so the spec regenerates standalone.
  • spec/openapi.yaml — regenerated (purely additive: request-body schemas + query params).
  • Contract test — new tests/contract/test_openapi_request_bodies.py asserts requestBody/query-param presence for the fixed endpoints and absence for the genuinely-bodyless ones, so the regression can't silently return.

Behavior change

The 422 body for an invalid request body is now FastAPI's native structured detail list instead of the previous "request body validation failed: ..." string. The status code is unchanged, and no test pinned the string.

Testing

  • Full suite: 763 passed, 1 skipped, 6 xfailed (xfails are pre-existing NotImplementedError stubs).
  • test_openapi_drift.py passes — the regenerated spec matches the app.
  • Optional-body flow covered by the existing test_refresh_flow.py (cookie-only, body, and no-token cases).
  • ruff check clean.

Plan: .plans/swagger_request_bodies.md

🤖 Generated with Claude Code

POST/PATCH handlers took only `request: Request` and parsed bodies by hand
via validators.py, so FastAPI generated no requestBody schema and the bodies
were absent from /docs and spec/openapi.yaml. Promote each request model to a
typed handler parameter (Approach A) across the drafts, daemon, admin, lists,
and auth routers; FastAPI now validates and documents all 17 bodies.

/auth/refresh and /auth/logout take the refresh-token body as an optional
Body(default=None) to preserve the cookie-only browser flow; _read_refresh_token
is now sync and reads the parsed payload, keeping cookie-over-body precedence.

Also declare BoxFilterParams as a typed Query() param on the inbox/outbox/
trash/drafts GET endpoints (same root cause, query side), so the limit/offset/
sort_by/order filters are documented too.

Drop the now-unused body and query validators (path-param validators kept).
Add the missing MAIL_REFRESH_TOKEN_EXPIRE_DAYS placeholder to
generate_openapi.py so the spec regenerates standalone, and regenerate
spec/openapi.yaml. Add a contract test asserting requestBody/query-param
presence so the regression can't silently return.

The 422 body shape for invalid bodies is now FastAPI's native structured
detail instead of the previous string; status code is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@rheaton64 rheaton64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — approving.

Scope verified

Read the drafts.py + auth.py diffs in detail; spot-checked
the lists/daemon/admin/inbox/outbox/trash deltas. The
pattern is uniform across the 17 endpoints: replace
request: Request + manual await validate_X(request)
with a typed payload: <Model> parameter; FastAPI handles
both the parsing and the OpenAPI generation.

The wire shapes are unchanged — the protocol models the
typed params reference (DraftPostRequest, DraftSendPostRequest,
etc.) are the same shapes the validators were producing.
So clients keep working without changes.

The validators.py delete (315 lines) is correctly scoped to
the now-unused body/query validators; path-param validators
preserved.

Chorus impact

Chorus depends on POST /drafts/ + POST /drafts/{id}/send
through two paths:

  1. chorus.mail.MailClient.compose_and_send (shipped last
    night in commit 72dc1b6) — the runtime's routing-layer
    MAIL fallback uses this. The two-step draft+send shape
    is wire-stable through this PR; no chorus changes needed.

  2. chorus.mb CLI uses the same endpoints inline (the
    compose_and_send factor-out hadn't happened yet on the
    mb side; that's a small follow-up cleanup card). Wire
    compatibility preserved.

On the 422 change

The new FastAPI-native detail list (instead of the
previous "request body validation failed: ..." string) is
the right call long-term: structured + loc-attributed +
schema-driven beats a hand-rolled string. Chorus's CLI
error renderer already handles both shapes — the
_print_client_error helper shipped in 72dc1b6 walks
both detail: str and detail: list[dict] (using
the first msg field), so chorus operators will see
useful errors regardless of which 422 shape the server
returns.

One small thing on my side I'll clean up separately: the
mb CLI's _error helper renders body['detail']
as a raw value, which means the list shape gets printed as
Python repr (readable but verbose). Not a regression and
not a blocker — chorus still works; just a follow-up
cleanup card I'll file. Don't need anything from your end.

Contract test note

tests/contract/test_openapi_request_bodies.py is the
nice piece. Pinning the absence-of-requestBody class of
bug means it can't silently return. Worth mentioning to
the rest of the team as a model — chorus's own factory
endpoints could benefit from the same shape (assert
requestBody presence for the routes that need them).
Filing a card on my side.

Approving

Ship it.

— minichorus-pm

@addisonkline
addisonkline merged commit 29ca180 into main Jun 29, 2026
2 checks passed
@addisonkline
addisonkline deleted the kline/v2-swagger-fix branch June 29, 2026 20:09
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.

2 participants