REST API: standardize error response body contract #46

Closed
opened 2026-06-28 10:42:40 +02:00 by timothy · 3 comments
Owner

Part of #2 (REST API tracker). Follow-up from #35 / PR #45 final review.

Problem

The REST API runtime and generated OpenAPI contract currently disagree on error response bodies for the #2a/#2b ApiResults convention:

  • Runtime: ApiResults.ToErrorResult() returns NotFoundObjectResult(error.Value) for NotFoundError and UnprocessableEntityObjectResult(error.Value) for other BaseError values.
  • OpenAPI: controller [ProducesResponseType] metadata without explicit body types causes 404/422 responses to be documented as ProblemDetails.

This affects generated clients and /docs contract accuracy across REST slices, not only collections.

Scope

Decide and implement one standard shape for REST API error responses:

  • Option A: keep the runtime plain error value shape and document 404/422 schemas explicitly as that shape.
  • Option B: change ApiResults to return ProblemDetails (or another structured envelope) and update tests/controllers/OpenAPI accordingly.

Acceptance Criteria

  • ApiResults runtime behavior and OpenAPI schemas agree for 404 and 422 responses.
  • Existing REST controllers from #2a and #2b are updated consistently.
  • Tests cover the chosen error body shape.
  • ErsatzTV/wwwroot/openapi/v1.json is regenerated.

Notes

This was intentionally left out of #35 because changing the shared error body contract is cross-cutting and should be handled deliberately across the REST API surface.

Part of #2 (REST API tracker). Follow-up from #35 / PR #45 final review. ## Problem The REST API runtime and generated OpenAPI contract currently disagree on error response bodies for the #2a/#2b `ApiResults` convention: - Runtime: `ApiResults.ToErrorResult()` returns `NotFoundObjectResult(error.Value)` for `NotFoundError` and `UnprocessableEntityObjectResult(error.Value)` for other `BaseError` values. - OpenAPI: controller `[ProducesResponseType]` metadata without explicit body types causes 404/422 responses to be documented as `ProblemDetails`. This affects generated clients and `/docs` contract accuracy across REST slices, not only collections. ## Scope Decide and implement one standard shape for REST API error responses: - Option A: keep the runtime plain error value shape and document 404/422 schemas explicitly as that shape. - Option B: change `ApiResults` to return `ProblemDetails` (or another structured envelope) and update tests/controllers/OpenAPI accordingly. ## Acceptance Criteria - `ApiResults` runtime behavior and OpenAPI schemas agree for 404 and 422 responses. - Existing REST controllers from #2a and #2b are updated consistently. - Tests cover the chosen error body shape. - `ErsatzTV/wwwroot/openapi/v1.json` is regenerated. ## Notes This was intentionally left out of #35 because changing the shared error body contract is cross-cutting and should be handled deliberately across the REST API surface.
Author
Owner

Implementation branch pushed for review: issue46-rest-api-error-contract.

Decision: use ProblemDetails as the REST API #2 404/422 body contract because /docs and wwwroot/openapi/v1.json are generated-client contracts for MCP/UI consumers.

What changed:

  • ApiResults now returns ProblemDetails for NotFoundError, other BaseError values, and Option<T>.None read misses.
  • Channel, collection, and smart-collection controller metadata explicitly documents ProblemDetails for 404/422 responses.
  • Channel playout reset 404 was brought into the same ProblemDetails contract.
  • Static OpenAPI contract tests verify all advertised media types point to #/components/schemas/ProblemDetails.
  • ErsatzTV/wwwroot/openapi/v1.json was regenerated.

Verification:

  • LSP diagnostics: clean on changed C# files.
  • dotnet build ErsatzTV.sln: passed, 0 warnings/errors.
  • TZ=UTC dotnet test ErsatzTV.sln -v normal: passed, build 0 warnings/errors.
  • Adversarial subagent review: no findings.
Implementation branch pushed for review: `issue46-rest-api-error-contract`. Decision: use `ProblemDetails` as the REST API #2 404/422 body contract because `/docs` and `wwwroot/openapi/v1.json` are generated-client contracts for MCP/UI consumers. What changed: - `ApiResults` now returns `ProblemDetails` for `NotFoundError`, other `BaseError` values, and `Option<T>.None` read misses. - Channel, collection, and smart-collection controller metadata explicitly documents `ProblemDetails` for 404/422 responses. - Channel playout reset 404 was brought into the same `ProblemDetails` contract. - Static OpenAPI contract tests verify all advertised media types point to `#/components/schemas/ProblemDetails`. - `ErsatzTV/wwwroot/openapi/v1.json` was regenerated. Verification: - LSP diagnostics: clean on changed C# files. - `dotnet build ErsatzTV.sln`: passed, 0 warnings/errors. - `TZ=UTC dotnet test ErsatzTV.sln -v normal`: passed, build 0 warnings/errors. - Adversarial subagent review: no findings.
Author
Owner

Done

What was done: Implemented the #46 contract decision in PR #47: REST API #2 404/422 bodies are standardized on ProblemDetails. Runtime helpers, channel/collection/smart-collection controller metadata, tests, docs, and static OpenAPI were updated consistently.

Root cause: #2a/#2b mixed two implicit contracts. ApiResults.ToErrorResult() returned plain BaseError.Value strings at runtime, while controller metadata/generated OpenAPI documented 404/422 as ProblemDetails. Generated clients consuming /docs therefore saw a different schema than the actual response body.

Files changed: ErsatzTV/Extensions/ApiResults.cs; channel, collection, and smart-collection API controllers; API/controller/OpenAPI contract tests; ErsatzTV/wwwroot/openapi/v1.json; docs/rest-api.md.

Verification: LSP diagnostics clean on changed C# files; dotnet build ErsatzTV.sln passed with 0 warnings/errors; TZ=UTC dotnet test ErsatzTV.sln -v normal passed; adversarial subagent review found no issues. Gitea Actions run 174 also completed Build & test (.NET) and EF migration integrity (SQLite + MySql) successfully; image push was skipped as expected for pull_request.

Deferred: Unrelated older API controllers outside #2a/#2b still have their existing 404 shapes. They were intentionally left outside this issue’s scope.

Follow-up issues: None opened.

Docs updated: docs/rest-api.md now records the ProblemDetails error body contract.

Status: PR #47 is open and mergeable. Per the requested workflow, I am leaving #46 open and pausing for review rather than closing/merging.

## Done **What was done**: Implemented the #46 contract decision in PR #47: REST API #2 404/422 bodies are standardized on `ProblemDetails`. Runtime helpers, channel/collection/smart-collection controller metadata, tests, docs, and static OpenAPI were updated consistently. **Root cause**: #2a/#2b mixed two implicit contracts. `ApiResults.ToErrorResult()` returned plain `BaseError.Value` strings at runtime, while controller metadata/generated OpenAPI documented 404/422 as `ProblemDetails`. Generated clients consuming `/docs` therefore saw a different schema than the actual response body. **Files changed**: `ErsatzTV/Extensions/ApiResults.cs`; channel, collection, and smart-collection API controllers; API/controller/OpenAPI contract tests; `ErsatzTV/wwwroot/openapi/v1.json`; `docs/rest-api.md`. **Verification**: LSP diagnostics clean on changed C# files; `dotnet build ErsatzTV.sln` passed with 0 warnings/errors; `TZ=UTC dotnet test ErsatzTV.sln -v normal` passed; adversarial subagent review found no issues. Gitea Actions run 174 also completed `Build & test (.NET)` and `EF migration integrity (SQLite + MySql)` successfully; image push was skipped as expected for `pull_request`. **Deferred**: Unrelated older API controllers outside #2a/#2b still have their existing 404 shapes. They were intentionally left outside this issue’s scope. **Follow-up issues**: None opened. **Docs updated**: `docs/rest-api.md` now records the `ProblemDetails` error body contract. **Status**: PR #47 is open and mergeable. Per the requested workflow, I am leaving #46 open and pausing for review rather than closing/merging.
Author
Owner

Closed

PR #47 has been merged into main as 0d101e79b1852ef82e4b9d455a7d3ad5a62f4ce6.

What was done: Standardized REST API #2 404/422 error response bodies on ProblemDetails; updated runtime helpers, channel/collection/smart-collection controller metadata, tests, docs, and regenerated static OpenAPI.

Root cause: Runtime returned plain BaseError.Value strings while generated OpenAPI documented ProblemDetails, creating an invalid generated-client contract.

Verification: Local LSP diagnostics clean; dotnet build ErsatzTV.sln passed; TZ=UTC dotnet test ErsatzTV.sln -v normal passed; adversarial subagent review found no issues; PR CI run 174 completed build/test and migration integrity successfully.

Deferred: Older unrelated API controllers outside #2a/#2b retain their existing 404 behavior.

Follow-up issues: None.

Docs updated: docs/rest-api.md.

## Closed PR #47 has been merged into `main` as `0d101e79b1852ef82e4b9d455a7d3ad5a62f4ce6`. **What was done**: Standardized REST API #2 404/422 error response bodies on `ProblemDetails`; updated runtime helpers, channel/collection/smart-collection controller metadata, tests, docs, and regenerated static OpenAPI. **Root cause**: Runtime returned plain `BaseError.Value` strings while generated OpenAPI documented `ProblemDetails`, creating an invalid generated-client contract. **Verification**: Local LSP diagnostics clean; `dotnet build ErsatzTV.sln` passed; `TZ=UTC dotnet test ErsatzTV.sln -v normal` passed; adversarial subagent review found no issues; PR CI run 174 completed build/test and migration integrity successfully. **Deferred**: Older unrelated API controllers outside #2a/#2b retain their existing 404 behavior. **Follow-up issues**: None. **Docs updated**: `docs/rest-api.md`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#46