85 lines
5.7 KiB
Markdown
85 lines
5.7 KiB
Markdown
---
|
|
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 (<status>).` |
|
|
|
|
`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) |