Status: Accepted Date: 2026-04-30 Deciders: CCE frontend team
The admin-cms makes ~75 HTTP calls across feature pages. Each backend response can fail with:
0— network failure400— FluentValidation ProblemDetails (field-keyed)401— session expired (must redirect to login)403— permission denied404— resource not found409— concurrency conflict OR duplicate (distinguished bytypeURN)5xx— server error
Some failures need cross-cutting handling (401 → redirect, 5xx → toast). Others need feature-domain handling (409/concurrency on Resource update → "someone else edited; reload"). A single global handler cannot distinguish, but per-call try/catch in every page bloats controllers.
Options:
| Option | Notes |
|---|---|
| Global interceptor only | Cross-cutting OK, but loses feature-level context |
| Per-call try/catch | Repetitive; no shared logic |
| Hybrid: interceptor for cross-cutting + per-feature wrapper | Both layers, no duplication |
Use a hybrid model:
-
Three functional
HttpInterceptorFns, registered onprovideHttpClient(withInterceptors([...])):correlationIdInterceptor— addsX-Correlation-IdUUID per request.authInterceptor— setswithCredentials: true, redirects to/auth/loginon 401 (skips/api/me).serverErrorInterceptor— toastserrors.serveron 5xx anderrors.forbiddenon 403.
-
Per-feature
*ApiServicewrappers that translate every error to a typedFeatureError:export type FeatureError = | { kind: 'concurrency' } | { kind: 'duplicate' } | { kind: 'validation'; fieldErrors: Record<string, string[]> } | { kind: 'not-found' } | { kind: 'forbidden' } | { kind: 'server' } | { kind: 'network' } | { kind: 'unknown'; status: number };
Each service method returns
Result<T> = { ok: true; value: T } | { ok: false; error: FeatureError }. The mapping functiontoFeatureError(HttpErrorResponse)lives incore/ui/error-formatter.tsand is used by every wrapper. -
Page controllers render error kinds through i18n:
('errors.' + error.kind) | translate. They never see rawHttpErrorResponse. They can branch onerror.kind === 'concurrency'to surface targeted UX (e.g., "reload to see latest").
Positive:
- A single
toFeatureErrormapping is the source of truth for what an HTTP response means. Page logic stays small. - 5xx and 401 handling is centralised — every page benefits without per-page wiring.
- New features cannot accidentally drop network or server errors; the wrapper catches everything.
Negative:
- The 5xx interceptor toasts on 5xx; the per-feature wrapper also returns
{ kind: 'server' }. Pages that surface a banner risk double-notification (banner + toast).- Mitigation: Pages that show a banner choose not to also call
toast.error()onserverkind. The plan documents this.
- Mitigation: Pages that show a banner choose not to also call
- The validation kind carries
fieldErrors; consumers must remember to use them. Most forms use them viamat-erroron each field.
Verification:
core/ui/error-formatter.spec.tscovers every mapping branch.- Each
*ApiService.spec.tsconfirms the URL + the relevant error path (404, 409, 5xx).