Error Codes¶
SASO surfaces every machine-recoverable failure as a stable identifier of the form SASO-<DOMAIN>-<NNNN>. Clients should branch on the code field, not on title or detail — those carry localised wording that may change between releases.
The catalogue is the contract referenced by ADR 0004.
Format¶
| Domain | Range | Owner area |
|---|---|---|
AUTH |
1xxx |
Login, OIDC / SAML provisioning, password change |
ITEM |
2xxx |
Items and item operations |
LABEL |
3xxx |
Label definition and PDF generation |
SHELF |
4xxx |
Shelf management |
INSTALL |
5xxx |
Web installer flow |
CONFIG |
6xxx |
system_setting and provider configuration |
FLAG |
7xxx |
Feature flag evaluation |
INFRA |
9xxx |
Database / network / unhandled exceptions |
Within each domain the four-digit suffix counts upward starting at 0001. Codes are append-only. A code that goes out of use is marked (deprecated) but never reassigned, so logs from older releases stay decodable.
Catalogue¶
AUTH — authentication & session¶
| Code | HTTP | Title | When it is raised |
|---|---|---|---|
SASO-AUTH-1001 |
401 | Invalid credentials | Username / password (or OIDC token) did not match an active member |
SASO-AUTH-1002 |
401 | Session expired | Session was valid but exceeded the idle / absolute timeout |
SASO-AUTH-1003 |
403 | CSRF token mismatch | Submitted CSRF token did not validate against the session token |
SASO-AUTH-1004 |
401 | Authentication required | Endpoint requires an authenticated principal but none was supplied |
SASO-AUTH-1005 |
403 | Access denied | Authenticated, but lacks the role or permission for the requested action |
SASO-AUTH-1006 |
503 | Authentication provider is misconfigured | An AuthProvider cannot drive a login because its stored configuration is incomplete or unreachable (e.g. discovery URL 404, expired SAML certificate). Operator-actionable; the affected provider stays disabled until the row is fixed |
SASO-AUTH-1007 |
400 | Authentication callback could not be matched to a pending request | OIDC state / SAML RelayState did not match the value the application stored on beginLogin(). Most often caused by an expired login attempt (cookies cleared between hops); rarely indicates an attempted CSRF on the callback |
SASO-AUTH-1008 |
400 | Authentication callback failed verification | The IdP response (OIDC token signature, SAML assertion signature, nonce, audience, expiry) failed verification |
INFRA — infrastructure¶
| Code | HTTP | Title | When it is raised |
|---|---|---|---|
SASO-INFRA-9000 |
500 | Internal server error | Catch-all for any uncaught exception that does not extend DomainException. The full stack is logged; the response body carries only traceId |
SASO-INFRA-9001 |
503 | Database unavailable | Could not connect to the configured DSN, or the connection dropped mid-transaction |
SASO-INFRA-9002 |
503 | Storage unavailable | Filesystem path required by the request was not writable / readable |
SASO-INFRA-9003 |
404 | Endpoint not found | API router could not match the request path against any operation declared in config/openapi.yaml |
SASO-INFRA-9004 |
405 | Method not allowed | API router matched the path but not the HTTP method; allowed methods are listed in the server log under context.allowed |
The remaining domains (ITEM, LABEL, SHELF, INSTALL, CONFIG, FLAG) reserve their numeric ranges and will be filled as M3-D and M4 land.
How clients see them¶
Every API response from /api/v1/* (and every new endpoint added under the Clean Architecture / DDD layout) is application/problem+json:
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://docs.willen-federation.org/error-codes#SASO-AUTH-1001",
"title": "Invalid credentials",
"status": 401,
"detail": "The submitted password did not match.",
"instance": "/api/v1/auth/login",
"code": "SASO-AUTH-1001",
"traceId": "1f9b3c8a-9e15-4d6a-8c2c-3d8f4f1f7a12"
}
code— branch on this field. It is stable across releases.traceId— UUIDv4 unique per request. The server log carries the same id underextra.traceId; operators paste it into support tickets so engineers can locate the full trace.title/detail— localised strings (English in M3-B; English + Japanese once M3-C ships). Treat as display text only.
Web screens render a friendly message plus the traceId. Internals (stack traces, SQL fragments, file paths) never leave the server.
Adding a new code¶
A new code touches three places in the same PR:
- Add a case to
Saso\Domain\Shared\ErrorCode(src/Domain/Shared/ErrorCode.php). - Add the row to the table above.
- Add
error.<code>.titleanderror.<code>.detailkeys totranslations/en.yamlandtranslations/ja.yaml. Detail strings can include placeholders such as{traceId}for runtime interpolation.
Throw a subclass of DomainException carrying the new code; the global handler resolves both fields against the request locale and renders the Problem Details payload automatically. Missing Japanese strings fall through to English; missing English strings fall through to the exception message.
Legacy code paths¶
Pre-M3 screens still surface ad-hoc strings (die('invalid csrftoken.'), Either::left('error/1/')). Those migrate to typed exceptions as each feature moves into src/. Until then the legacy responses keep their existing shape — the catalogue above governs everything new.
See also¶
- ADR 0004 — RFC 7807 Problem Details +
SASO-DOMAIN-NNNNcodes - API Reference — request / response shape that surrounds error codes
- Security — disclosure policy and operator hardening