case-detail hard-500s on unknown plugin in a user-layer composition override (graceful-degradation gap, ADR-021)

Summary

GET /cases/{household_id} (worker portal case-detail) returns a hard 500 for a worker whenever that worker has a user-layer composition_documents override (surface case_detail) that references a plugin slug not registered via #[canopy_plugin]. The composition loader raises the ADR-021 closed-set UnknownPlugin error and fails the ENTIRE page render, rather than degrading gracefully.

Evidence

  • canopy-web log on every such request: case_detail load_composition failed — plugin slug 'X' referenced by composition is not registeredresponse failed — Status code: 500.
  • Reproduced deterministically: dashboard / returns 200 for the same session; /cases/{hh} returns 500 in ~2ms (fails before any downstream call).

Why it matters (production availability)

A single bad user-layer override bricks that user's entire case-detail page. User overrides are writable via the Customize-my-dashboard surface (ADR-024) and Studio (ADR-022). ADR-021's design intent is graceful degradation ('optional services degrade gracefully'; closed-set composition errors). A user-layer section that can't resolve should be skipped/dropped (and logged), not 500 the page.

Two candidate fixes (decide in the MR)

  1. Read-time (loader): in the case-detail composition loader, skip an unresolvable section from a user (and arguably role) layer rather than erroring; keep hard-fail only for the jurisdiction baseline. Aligns with ADR-021 graceful degradation.
  2. Write-time (validation): reject PUT /v1/composition/case_detail/user/me ops whose added slug is not in the baseline/registry (case_detail uses RFC 6902 ops, not user_delta_v1, so it bypasses the ADR-024 user_delta slug validation that dashboard surfaces get). 422 on write.
    • Likely want BOTH: validate on write AND degrade on read (defense in depth).

How it surfaced

tests/e2e/specs/composition-api.spec.ts writes a user/me case_detail override with a fake slug case-header-stub and (historically) never deleted it, 500ing every later case-detail render for that caseworker and failing ~25 case-detail E2E tests order-dependently. The test-isolation half is being fixed separately (cleanup via the now-existing DELETE /user/me route); THIS issue tracks the underlying loader/validation robustness gap.

Relevant files

  • services/canopy-web/src/api/case_detail.rs (load_composition 500 path)
  • services/canopy-web/src/api/composition.rs (PUT user/me write path; delete_user_me at ~:1083)
  • rulesets/georgia/composition/case_detail.toml (baseline slugs)
  • ADR-021 (closed-set composition errors / graceful degradation), ADR-024 (user_delta validation for dashboard surfaces)