Skip to content

Commit 96af25b

Browse files
committed
feat(backend): standardize RFC 7807 error envelopes (closes #254)
Replace ad-hoc `{success: false, error: ...}` responses across error- producing middleware and select controllers with the canonical RFC 7807 ProblemDetails envelope emitted by the central error handler. **Wire contract** Every 4xx/5xx response is now: ```http Content-Type: application/problem+json ``` ```json { "type": "https://aethermint.io/problems/...", "title": "Validation Error", "status": 400, "detail": "...", "instance": "POST /api/auth/register", "code": "VALIDATION_ERROR", "success": false, "requestId": "<uuid-v4>", "timestamp": "ISO-8601", "errors": [{ "field": "email", "message": "..." }], "error": { /* deprecated legacy mirror */ } } ``` **Acceptance criteria** - [x] Standard error JSON schema (type, title, status, detail, instance) - [x] Error code catalog (see backend/docs/ERROR_CATALOG.md) with stable URIs and machine-readable codes (Issue #254, \#127) - [x] Middleware that wraps all responses (backend/src/middleware/errorHandler.ts) - [x] Migration of existing error responses (hot path middleware + representative controllers, see Migration scope below) - [x] Tests verifying error format consistency (backend/tests/middleware/errorHandler.test.ts, ~14 cases) **Migration scope** Middleware (all on the request hot path): - validation.ts, validation.js (Joi + express-validator) - auth.js (JWT, RBAC, permission gates) - permissions.js, tenant.js (multi-tenant) - security.ts (DDOS, bot detection, blacklist, geo/time) - ipfsAuth.js, apiKey.js, rateLimiter.js (auth & throttling) - sanitizer.ts, idempotency.ts - shutdown.ts (`shutdownGuard` now emits RFC 7807 503) Controllers (representative, AppError throwers): - courseController.ts (every catch block delegates to the central handler; handler signatures updated to accept `next`) - bulkOperationsController.ts (inline 400s → ValidationError with field-level details) - holographicController.ts, analyticsController.js (catch blocks) **Backward compatibility** The central handler still mirrors the legacy `{success:false, error:{...}}` shape under body.error for older CLIs that depend on it. The mirror is `deprecated: true` in OpenAPI and scheduled for removal in the next major version. See `backend/docs/ERROR_CATALOG.md` §4 for the field-mapping table. **OpenAPI** `backend/src/config/swagger.ts` and `backend/src/docs/openapi.ts` now register: - Component schemas: `ProblemDetails`, `FieldValidationError`, legacy `Error` (deprecated). - Reusable responses: `Problem400/401/403/404/409/413/415/429/500/503`. - All paths use these references for error documentation. **Tests** - backend/tests/middleware/errorHandler.test.ts (new, 14 cases): contract, content-type, catalog routing, validation details, headersSent guard, unknown-code fallback, catchAsync plumbing, legacy mirror, instance format, dev-only stack traces. - backend/tests/middleware/auth.test.js: updated IPFS auth expectations from `res.status().json({success:false,...})` to `next(new AppError(...))` so existing assertions still pass. Refs: #254 (RFC 7807), #127 (central error handler foundation)
1 parent 1677837 commit 96af25b

24 files changed

Lines changed: 1650 additions & 1550 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)