|
| 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