5.7 KiB
5.7 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| guide | Notifications | SPA-wide notification store + the global HTTP error interceptor that translates backend error codes and statuses into friendly user-facing messages. |
|
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:
provideHttpClient(
withInterceptors([csrfInterceptor, authInterceptor, errorNotificationInterceptor]),
)
The interceptor:
- Catches every
HttpErrorResponsethrown from any HTTP call. - Maps the response to a friendly message using
friendlyMessage(status, err.error). - Suppresses notifications for endpoints that have their own user-facing error presentation (see Suppression rules below).
- Pushes the message via
NotificationService.error(...). - Re-throws the original error so the caller still receives the
ApiErrorEnvelope(e.g.ChallengesApiService.submitreadsenvelope.codeto 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 (uses
errorNotificationInterceptorfor non-flag submission failures) - Authenticated Shell (cross-tab
invalidation propagates from a 401 SSE response via
NotificationServiceis not triggered — the shell uses its own peer-invalidation channel) - REST API Overview (error envelope)