Skip to content

feat(api): The OpenAPI document describes every response (#365) - #380

Merged
bitbiter-dev merged 6 commits into
masterfrom
feat/365-openapi-responses
Oct 30, 2026
Merged

bitbiter-dev merged 6 commits into
masterfrom
feat/365-openapi-responses

Conversation

@bitbiter-dev

@bitbiter-dev bitbiter-dev commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #365

Summary

Every operation in docs/openapi/v1.json now lists every status it can return, each with its body schema.

MapApiEndpoints
  api group
    Finally(DescribeErrorsAsProblems)   # body routes get 400/415; any error still bodiless → ProblemDetails
    Users (admin)   .RequireAdmin()            → 401, 403
    Media/Flashbacks/Bursts .RequireUser()     → 401, 403 (must change password)
    me, me/password, logout .RequireUserAllowedWhileMustChangePassword() → 401 only
    rate-limited routes .RequireRateLimit(p)   → 429
    each route: Produces<T>/ProducesValidationProblem/Produces(404)…
 Program.cs
 app.UseForwardedHeaders();
+app.UseApiProblems();   # exception handler → problem 500; under /api, bodiless 401/403/404/409/400/415/501 → problem body

Decided in session, beyond #365's text:

Also: named TokenResponse (was anonymous), a code property in the problem schemas, and codes for the work-item and proxy-sweep validation problems (#365 comment).

Wire changes for clients (nothing is deployed):

  • Former empty 401/403/404/409/400/415/501 responses carry application/problem+json bodies.
  • Work-item and proxy-sweep errors values are codes (invalid_page, invalid_page_size, pending_or_outcome, orphan_ids_or_all, invalid_orphan_count, unknown_orphans); the English text moved to detail. The field key OrphanIds is now orphanIds.

Evidence

  • Before: every operation in v1.json was "200": { "description": "OK" } with no schema.
    After: ApiDocumentTests.Operation_DescribesEveryResponseItCanGive pins a literal status → schema row for each of the 29 operations (failed 28/29 before the change). Responses_CoverEveryOperation fails when a route has no row.
  • Guards in RouteTableTests: a route with no responses of its own fails (shown failing on an undeclared GET /users), and so does a protected route without 401 or a rate-limited route without 429 (shown failing on a bare RequireAuthorization()).
  • HTTP seam (AuthenticatedTestApp, now with problem details, status code pages and the exception handler): 400/401/403/404/412/415/500/501 come back as problem+json; 416 has no body; the token JSON is exact (refreshToken only for mobile).
  • One direct test of the convention (EndpointResponsesTests), added only to kill a Last() mutant the document seam can't reach.
  • scripts/mutation-changed.sh: 100.00% on every changed API file. scripts/derive-openapi.sh --check: current.

Merge Danger

Door: two-way

Blast Radius: API error bodies

All error responses change shape, which matters for the UI client (#345, not built yet) and the native app. Merged with #359: UseApiProblems scopes the status code pages to /api, so UI asset 404s keep the empty body ui-serving.md specifies (the UI serving tests now run behind it). #340 must update the proxy path's row in ApiDocumentTests.

🤖 Generated with Claude Code

bitbiter-dev and others added 6 commits October 30, 2026 19:10
Every route declares its success bodies and errors. RequireUser,
RequireAdmin and RequireRateLimit declare the 401, 403 and 429 they
cause, once per group where the group carries the requirement.

Errors that had no body (challenge 401, policy 403, NotFound, Conflict,
binding 400/415, the 501 stubs) are now Problem Details through
UseStatusCodePages, and a Finally convention describes them that way.
The proxy's 416 keeps its empty body and is declared so.

Login and refresh return a named TokenResponse, the problem schemas
carry the code extension, and the work-item and proxy-sweep validation
problems get codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The must-change-password middleware lets anonymous endpoints through, so
sign-in and refresh never answer the 403 the document leaves out for
them. RequireUser sits on the Media, Flashbacks and Bursts groups, and
the tests share one invalid-field assertion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it causes (#365)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… their empty 404s (#365)

#359's UI serving gives a missing asset an empty 404. UseApiProblems
scopes the status code pages to /api, and the UI serving tests run
behind it as production does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bitbiter-dev
bitbiter-dev merged commit fa6907c into master Oct 30, 2026
20 checks passed
@bitbiter-dev
bitbiter-dev deleted the feat/365-openapi-responses branch October 30, 2026 07:22
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.

feat(api): The OpenAPI document describes every response

1 participant