--- type: guide title: Notifications description: SPA-wide notification store + the global HTTP error interceptor that translates backend error codes and statuses into friendly user-facing messages. tags: [guide, notifications, error, interceptor, toast, ux] timestamp: 2026-07-23T00:10:00Z --- # Components | Symbol | Path | Responsibility | |-------------------------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. | | `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that catches `HttpErrorResponse`, looks up the error code, and pushes a friendly message into `NotificationService`. | | `NotificationService.messages` | signal (consumed by future toast UI) | Currently the messages signal is appended in-memory only; the list itself doubles as the integration point for a future toast/banner component. Each push also mirrors the message to the browser console (`console.error` for `error`, `console.info` for `info`). | # Wiring `frontend/src/main.ts` registers the interceptor **after** `authInterceptor` so the auth header is already attached when the error interceptor sees the request: ```ts provideHttpClient( withInterceptors([csrfInterceptor, authInterceptor, errorNotificationInterceptor]), ) ``` The interceptor: 1. Catches every `HttpErrorResponse` thrown from any HTTP call. 2. Maps the response to a friendly message using `friendlyMessage(status, err.error)`. 3. Suppresses notifications for endpoints that have their own user-facing error presentation (see *Suppression rules* below). 4. Pushes the message via `NotificationService.error(...)`. 5. Re-throws the original error so the caller still receives the `ApiErrorEnvelope` (e.g. `ChallengesApiService.submit` reads `envelope.code` to render the modal's inline error). # Friendly message mapping `friendlyMessage(status, body)` resolves in this order: | Source | Message | |-----------------------------------------------------|------------------------------------------------------------------------------| | `status === 0` (network / CORS / aborted) | `Network error. Please try again.` | | `body.code` matches `FRIENDLY_MESSAGES` | The mapped friendly text (see table below). | | `status === 401` or `403` | `Your session is no longer valid.` (unless suppressed). | | `status === 404` | `Not found.` | | `status >= 500` | `Server error. Please try again later.` | | `body.message` (non-empty) | Falls back to that message. | | otherwise | `Request failed ().` | `FRIENDLY_MESSAGES` lookup table: | Error code | Friendly text | |-----------------------|------------------------------------------------------------| | `EVENT_NOT_RUNNING` | `Submissions are only accepted while the event is running.` | | `FLAG_INCORRECT` | `Incorrect flag.` | | `UNAUTHORIZED` | `Your session is no longer valid.` | | `FORBIDDEN` | `You are not allowed to perform this action.` | | `NOT_FOUND` | `Not found.` | | `VALIDATION_FAILED` | `Invalid input.` | # Suppression rules Some errors are expected and have their own UI. To avoid double-toasting: | URL pattern | Status | Why | |---------------------------------------|--------|---------------------------------------------------------------------------------------------------| | `/api/v1/auth/login` `/csrf` `/register` | `401` | A failed login or first-fetch of the CSRF cookie produces a `401`; the form already renders the inline error. | | `/api/v1/challenges/status` | `401` | The challenges page uses the snapshot as a bootstrap hint only — the SSE stream (`/api/v1/events`) is the source of truth and will deliver the next frame. | In both cases the interceptor still re-throws the error so callers can react locally; only the notification push is skipped. # See also - [Challenges Board](/guides/challenges-board.md) (uses `errorNotificationInterceptor` for non-flag submission failures) - [Authenticated Shell](/guides/authenticated-shell.md) (cross-tab invalidation propagates from a 401 SSE response via `NotificationService` is **not** triggered — the shell uses its own peer-invalidation channel) - [REST API Overview](/api/rest-overview.md) (error envelope)