Errors and Status Codes
The canonical error envelope shared by REST and MCP, plus the HTTP status → code mapping.
Both REST and MCP emit the same error shape. Source: core-api/src/core_api/errors.py (added in PR #58).
Canonical envelope
{
"error": {
"code": "<UPPER_SNAKE>",
"message": "<human-readable>",
"details": { "...": "optional" }
}
}Dispatch on error.code — it's the machine-readable signal and is identical across both surfaces.
REST back-compat
REST responses keep a top-level detail field alongside error so existing clients reading response.json()["detail"] keep working:
{
"detail": "Memory not found",
"error": { "code": "NOT_FOUND", "message": "Memory not found" }
}detail is the deprecated mirror. New clients should switch to error.code.
For FastAPI request-validation failures (422), detail is the original list of validation errors and error.details.errors carries the same list aggregated.
MCP envelope
MCP tools return the same error envelope, JSON-serialized, plus _latency_ms:
{
"error": {
"code": "INVALID_ARGUMENTS",
"message": "Unknown op 'wat'.",
"details": { "op": "wat", "expected_ops": ["read", "update", "..."] }
},
"_latency_ms": 7
}Pre-PR-#58 MCP tools returned bare "Error (XXX): ..." strings — that format is gone.
HTTP status → canonical code
From STATUS_TO_CODE in errors.py:
| Status | Code |
|---|---|
| 400 | BAD_REQUEST |
| 401 | UNAUTHORIZED |
| 402 | PAYMENT_REQUIRED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 405 | METHOD_NOT_ALLOWED |
| 408 | REQUEST_TIMEOUT |
| 409 | CONFLICT |
| 410 | GONE |
| 413 | PAYLOAD_TOO_LARGE |
| 415 | UNSUPPORTED_MEDIA_TYPE |
| 422 | INVALID_ARGUMENTS |
| 429 | RATE_LIMITED |
| 500 | INTERNAL_ERROR |
| 501 | NOT_IMPLEMENTED |
| 502 | UPSTREAM_ERROR |
| 503 | UNAVAILABLE |
| 504 | UPSTREAM_TIMEOUT |
Statuses not in the table fall back to HTTP_<status> (e.g. HTTP_418).
Trust-level errors
Trust failures come from core_api.services.trust_service.parse_trust_error and are re-wrapped as canonical FORBIDDEN with the required vs. caller's level in details.
Idempotency
The write route (POST /api/v1/memories) accepts an Idempotency-Key header (IDEMPOTENCY_HEADER in core-api/src/core_api/middleware/idempotency.py). A retry within the cache window with the same key short-circuits to the original response without consuming a write slot.