fix: document request bodies and box query params in OpenAPI - #83
Conversation
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
left a comment
There was a problem hiding this comment.
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:
-
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. -
chorus.mbCLI 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
Summary
POST/PATCH handlers took only
request: Requestand parsed their bodies by hand viavalidators.py, so FastAPI generated norequestBodyschema — the bodies were absent from/docs,/openapi.json, and the committedspec/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, viaOAuth2PasswordRequestForm) was the model for the fix.While here — folded in the same root cause on the query side: the
GET /inbox|/outbox|/trash|/draftsendpoints now declareBoxFilterParamsas a typedQuery()param, so theirlimit/offset/sort_by/orderfilters are documented too.Changes
drafts,daemon,admin,lists,auth; declaredBoxFilterParamsquery params on the box GETs. List policy guardrails preserved./auth/refreshand/auth/logouttake the refresh-token body asBody(default=None)to preserve the cookie-only browser flow;_read_refresh_tokenis 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 missingMAIL_REFRESH_TOKEN_EXPIRE_DAYSplaceholder (latent bug from the refresh-token merge) so the spec regenerates standalone.spec/openapi.yaml— regenerated (purely additive: request-body schemas + query params).tests/contract/test_openapi_request_bodies.pyassertsrequestBody/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
detaillist instead of the previous"request body validation failed: ..."string. The status code is unchanged, and no test pinned the string.Testing
NotImplementedErrorstubs).test_openapi_drift.pypasses — the regenerated spec matches the app.test_refresh_flow.py(cookie-only, body, and no-token cases).ruff checkclean.Plan:
.plans/swagger_request_bodies.md🤖 Generated with Claude Code