Skip to content

Commit 1e788b5

Browse files
authored
Merge pull request #347 from danfulani0/fix/issue-254-rfc-7807-error-envelope
[Backend] Standardize RFC 7807 ProblemDetails error envelopes (#254)
2 parents e413d41 + ca1bdeb commit 1e788b5

26 files changed

Lines changed: 1993 additions & 1579 deletions

backend/docs/ERROR_CATALOG.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# AetherMint Error Catalog (RFC 7807)
2+
3+
> Issue #254_Standardize error response format across all API endpoints_
4+
5+
This catalog is the **single source of truth** for every machine-readable
6+
error code the AetherMint API can emit. New codes must be added here before
7+
they ship in code, and every code below must resolve to exactly one row in
8+
`backend/src/utils/problemDetails.ts → ErrorCatalog`.
9+
10+
All error responses follow
11+
[RFC 7807 — Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc7807)
12+
and are served with `Content-Type: application/problem+json`. Every
13+
endpoint emits the same canonical envelope so clients can implement one
14+
uniform error handler regardless of route.
15+
16+
---
17+
18+
## 1. Wire format
19+
20+
| Field | Type | Notes |
21+
| ------------ | -------- | ------------------------------------------------- |
22+
| `type` | `string` (URI) | Stable identifier of the problem class |
23+
| `title` | `string` | Short summary, constant for the same `type` |
24+
| `status` | `number` | Mirror of the HTTP status code |
25+
| `detail` | `string` | Per-occurrence explanation (human-readable) |
26+
| `instance` | `string` | `"<METHOD> <path>"` of the failing request |
27+
| `code` | `string` | Machine-readable AetherMint code (see catalog) |
28+
| `success` | `false` | Always `false` for an error response |
29+
| `requestId` | `string` | UUID v4; matches the `X-Request-ID` response header |
30+
| `timestamp` | `string` | ISO-8601 string |
31+
| `errors?` | `array` | Field-level validation errors (see schema) |
32+
| `error` | `object` | **Deprecated** legacy envelope mirror (see §4) |
33+
34+
### Example envelope — `POST /api/auth/register` with bad payload
35+
36+
```http
37+
HTTP/1.1 400 Bad Request
38+
Content-Type: application/problem+json
39+
X-Request-ID: 7e2c1f5a-8d2b-4e0d-9d6f-3a1d2e9b4c10
40+
```
41+
42+
```json
43+
{
44+
"type": "https://aethermint.io/problems/validation-error",
45+
"title": "Validation Error",
46+
"status": 400,
47+
"detail": "Validation failed for 2 fields",
48+
"instance": "POST /api/auth/register",
49+
"code": "VALIDATION_ERROR",
50+
"success": false,
51+
"requestId": "7e2c1f5a-8d2b-4e0d-9d6f-3a1d2e9b4c10",
52+
"timestamp": "2026-07-24T12:34:56.000Z",
53+
"errors": [
54+
{ "field": "email", "message": "\"email\" must be a valid email" },
55+
{ "field": "password", "message": "\"password\" length must be at least 8 characters long" }
56+
],
57+
"error": {
58+
"code": "VALIDATION_ERROR",
59+
"message": "Validation failed for 2 fields",
60+
"details": [
61+
{ "field": "email", "message": "\"email\" must be a valid email" },
62+
{ "field": "password", "message": "\"password\" length must be at least 8 characters long" }
63+
],
64+
"requestId": "7e2c1f5a-8d2b-4e0d-9d6f-3a1d2e9b4c10"
65+
}
66+
}
67+
```
68+
69+
---
70+
71+
## 2. Catalog
72+
73+
| `code` | HTTP | Title | `type` URI | Default message |
74+
| ----------------------- | ---- | ------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
75+
| `VALIDATION_ERROR` | 400 | Validation Error | `https://aethermint.io/problems/validation-error` | The request payload failed validation. |
76+
| `UNAUTHORIZED` | 401 | Unauthorized | `https://aethermint.io/problems/unauthorized` | Authentication is required to access this resource. |
77+
| `FORBIDDEN` | 403 | Forbidden | `https://aethermint.io/problems/forbidden` | You do not have permission to perform this action. |
78+
| `NOT_FOUND` | 404 | Not Found | `https://aethermint.io/problems/not-found` | The requested resource could not be found. |
79+
| `CONFLICT` | 409 | Conflict | `https://aethermint.io/problems/conflict` | The request conflicts with the current state of the resource. |
80+
| `PAYLOAD_TOO_LARGE` | 413 | Payload Too Large | `https://aethermint.io/problems/payload-too-large` | The request body exceeds the maximum allowed size. |
81+
| `UNSUPPORTED_MEDIA_TYPE`| 415 | Unsupported Media Type | `https://aethermint.io/problems/unsupported-media-type` | The request media type is not supported by this endpoint. |
82+
| `RATE_LIMITED` | 429 | Too Many Requests | `https://aethermint.io/problems/rate-limited` | You have exceeded the rate limit. Please retry after a moment. |
83+
| `SERVICE_UNAVAILABLE` | 503 | Service Unavailable | `https://aethermint.io/problems/service-unavailable` | The service is temporarily unavailable. Please retry shortly. |
84+
| `INTERNAL_ERROR` | 500 | Internal Server Error | `https://aethermint.io/problems/internal-error` | An unexpected error occurred. Please try again later. |
85+
| _fallback_ | 500 | Unknown Error | `https://aethermint.io/problems/unknown-error` | An unspecified error occurred. (only used for codes absent from this table) |
86+
87+
> Every `code` in the left column must match an entry in
88+
> [`backend/src/utils/problemDetails.ts → ErrorCatalog`](../src/utils/problemDetails.ts).
89+
> Adding a new operational error requires **both** a new AppError
90+
> subclass in `utils/errors.ts` **and** a new entry in `ErrorCatalog`,
91+
> otherwise the response will fall through to `UNKNOWN_ERROR`.
92+
93+
---
94+
95+
## 3. Status / severity policy
96+
97+
- `400–499` are **operational** errors. Logged at `warn` level.
98+
- `500–599` and any non-operational throw are **programmer / infra**
99+
errors. Logged at `error` level with full stack trace (development only).
100+
- The central error middleware always sets `Content-Type:
101+
application/problem+json` and writes the envelope via `res.send`.
102+
- Stack traces are only included when `NODE_ENV === 'development'`.
103+
104+
---
105+
106+
## 4. Backward-compatibility mirror
107+
108+
The legacy `{ "success": false, "error": { "code", "message", "details",
109+
"requestId" } }` shape is still emitted under the top-level `error` field
110+
so existing CLI / dashboard clients keep working. The mirror is annotated
111+
`deprecated: true` in the OpenAPI schema and is scheduled for removal in
112+
the next major version.
113+
114+
When migrating clients:
115+
116+
| Old field | New field |
117+
| ------------------- | -------------------- |
118+
| `body.success` | `body.success` *(unchanged, always `false`)* |
119+
| `body.error.code` | `body.code` |
120+
| `body.error.message`| `body.detail` |
121+
| `body.error.details`| `body.errors` |
122+
| _none_ | `body.type` |
123+
| _none_ | `body.title` |
124+
| _none_ | `body.instance` |
125+
| _none_ | `body.timestamp` |
126+
| `body.error.requestId` | `body.requestId` |
127+
128+
---
129+
130+
## 5. Adding a new error
131+
132+
1. Create the subclass in `backend/src/utils/errors.ts`.
133+
2. Add a matching row in `ErrorCatalog` (`utils/problemDetails.ts`) — never
134+
throw with a `code` that does not exist there.
135+
3. Update the table in this document.
136+
4. Add a test in `backend/tests/middleware/errorHandler.test.ts`.
137+
5. Reference the new `type` URI in any client SDK error mapping.
138+
139+
Thanks to one error middleware emitting the same shape everywhere,
140+
no other code changes are typically required for new codes to flow
141+
through every route.

0 commit comments

Comments
 (0)