2026-03-27 14:05:14 +08:00
|
|
|
# HTTP Error Contract (RFC7807 + Stable Codes)
|
|
|
|
|
|
|
|
|
|
This document is the single source of truth for backend HTTP error transport format and frontend parsing strategy.
|
|
|
|
|
|
|
|
|
|
## Response Format
|
|
|
|
|
|
|
|
|
|
All API errors must use `application/problem+json` and include RFC7807 fields.
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"type": "about:blank",
|
|
|
|
|
"title": "Unprocessable Entity",
|
|
|
|
|
"status": 422,
|
|
|
|
|
"detail": "Validation failed",
|
|
|
|
|
"code": "TODO_TITLE_REQUIRED",
|
|
|
|
|
"params": {
|
|
|
|
|
"field": "title"
|
|
|
|
|
},
|
|
|
|
|
"instance": "/api/v1/todo"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Field Rules
|
|
|
|
|
|
|
|
|
|
- `code` (required for business errors): stable machine-readable code (`UPPER_SNAKE_CASE`)
|
|
|
|
|
- `params` (optional): key-value values for localized message placeholders
|
|
|
|
|
- `detail` (required by RFC7807): human-readable fallback/debug text
|
|
|
|
|
|
|
|
|
|
## Backend Rules
|
|
|
|
|
|
|
|
|
|
- Do not rely on free-text `detail` as the only contract.
|
|
|
|
|
- New endpoints and new error branches must return stable `code`.
|
|
|
|
|
- Existing branches can migrate incrementally but must prefer code-first.
|
|
|
|
|
- Keep status semantics unchanged (`400/401/403/404/409/422/429/5xx`).
|
|
|
|
|
|
|
|
|
|
## Frontend Parsing Rules
|
|
|
|
|
|
|
|
|
|
- Parse in this order: `code` -> `params` -> `status` -> fallback `detail`.
|
|
|
|
|
- User-facing text should come from local l10n mapping by `code`.
|
|
|
|
|
- Unknown code fallback:
|
|
|
|
|
1) status-based generic localized message
|
|
|
|
|
2) safe fallback localized message (do not expose raw internals)
|
|
|
|
|
|
|
|
|
|
## Error Code Registry (Single Source of Truth)
|
|
|
|
|
|
|
|
|
|
This section is the canonical registry shared by backend and frontend.
|
|
|
|
|
|
|
|
|
|
When creating/modifying/deprecating any code, this table must be updated in the same change.
|
|
|
|
|
|
|
|
|
|
| Code | Domain | HTTP | Meaning |
|
|
|
|
|
|---|---|---:|---|
|
|
|
|
|
| `AGENT_RUN_INPUT_INVALID` | agent | 422 | Run input payload invalid |
|
|
|
|
|
| `AGENT_RUN_MESSAGES_INVALID` | agent | 422 | Run messages contract invalid |
|
2026-03-30 09:07:07 +08:00
|
|
|
| `AGENT_INVALID_RUN_ID` | agent | 422 | SSE runId query invalid |
|
2026-03-27 14:05:14 +08:00
|
|
|
| `AGENT_INVALID_LAST_EVENT_ID` | agent | 422 | SSE Last-Event-ID invalid |
|
|
|
|
|
| `AGENT_SSE_CONNECTION_LIMIT` | agent | 429 | SSE connections exceed per-user limit |
|
|
|
|
|
| `AGENT_ATTACHMENT_EMPTY` | agent | 422 | Attachment payload empty |
|
|
|
|
|
| `AGENT_ATTACHMENT_TOO_LARGE` | agent | 413 | Attachment exceeds allowed size |
|
|
|
|
|
| `AGENT_AUDIO_UNSUPPORTED_FORMAT` | agent | 400 | Audio content type/header unsupported |
|
|
|
|
|
| `AGENT_AUDIO_TOO_LARGE` | agent | 400 | Audio exceeds allowed size |
|
|
|
|
|
| `AGENT_AUDIO_EMPTY` | agent | 400 | Audio payload empty |
|
|
|
|
|
| `AGENT_ASR_UNAVAILABLE` | agent | 502 | ASR dependency unavailable |
|
|
|
|
|
| `AGENT_FORBIDDEN` | agent | 403 | Current user does not own target thread/session |
|
|
|
|
|
| `AGENT_PAYLOAD_INVALID` | agent | 422 | Run payload or forwarded runtime mode is invalid |
|
|
|
|
|
| `AGENT_ATTACHMENTS_TOO_MANY` | agent | 422 | Attachments exceed per-message limit |
|
|
|
|
|
| `AGENT_SIGNED_IMAGE_URL_INVALID` | agent | 422 | Signed image URL is malformed or unverifiable |
|
|
|
|
|
| `AGENT_ATTACHMENT_STORAGE_UNAVAILABLE` | agent | 503 | Attachment storage backend unavailable |
|
|
|
|
|
| `AGENT_ATTACHMENT_UNSUPPORTED_TYPE` | agent | 422 | Attachment MIME type is unsupported |
|
|
|
|
|
| `AGENT_ATTACHMENT_UPLOAD_FAILED` | agent | 502 | Upload to attachment storage failed |
|
|
|
|
|
| `AGENT_ATTACHMENT_BUCKET_INVALID` | agent | 422 | Attachment bucket does not match allowed bucket |
|
|
|
|
|
| `AGENT_ATTACHMENT_PATH_SCOPE_INVALID` | agent | 422 | Attachment path is outside allowed user scope |
|
|
|
|
|
| `AGENT_SIGNED_URL_GENERATION_FAILED` | agent | 502 | Failed to generate signed URL from storage backend |
|
|
|
|
|
| `AGENT_SESSION_ID_INVALID` | agent | 422 | Session ID is not a valid UUID |
|
|
|
|
|
| `AGENT_SESSION_NOT_FOUND` | agent | 404 | Agent chat session does not exist |
|
|
|
|
|
| `AGENT_USER_ID_INVALID` | agent | 422 | User ID is not a valid UUID |
|
|
|
|
|
| `INVALID_BINARY_URL_HOST` | agent | 422 | Signed URL host is invalid |
|
|
|
|
|
| `INVALID_BINARY_URL_BUCKET` | agent | 422 | Signed URL bucket is invalid |
|
|
|
|
|
| `INVALID_BINARY_URL_PATH_SCOPE` | agent | 422 | Signed URL path scope is invalid |
|
|
|
|
|
| `AUTH_SERVICE_UNAVAILABLE` | auth | 503 | Upstream auth service is temporarily unavailable |
|
|
|
|
|
| `AUTH_TOO_MANY_REQUESTS` | auth | 429 | Auth operation exceeds request rate limit |
|
|
|
|
|
| `AUTH_VERIFICATION_CODE_INVALID` | auth | 401 | OTP verification code is invalid |
|
|
|
|
|
| `AUTH_REFRESH_TOKEN_INVALID` | auth | 401 | Refresh token is invalid or expired |
|
|
|
|
|
| `AUTH_REFRESH_TOKEN_MISSING` | auth | 401 | Refresh token is missing for logout/refresh |
|
|
|
|
|
| `AUTH_USER_NOT_FOUND` | auth | 404 | User lookup by phone returns no match |
|
|
|
|
|
| `AUTH_UNAUTHORIZED` | auth | 401 | Authorization header or token is invalid |
|
|
|
|
|
| `JWT_VERIFIER_NOT_CONFIGURED` | auth | 503 | JWT verifier configuration is missing |
|
|
|
|
|
| `AUTOMATION_JOB_LIMIT_EXCEEDED` | automation_jobs | 400 | User-created automation jobs exceed allowed limit |
|
|
|
|
|
| `AUTOMATION_SYSTEM_JOB_MODIFICATION_FORBIDDEN` | automation_jobs | 403 | System bootstrap job cannot be modified |
|
|
|
|
|
| `AUTOMATION_JOB_NOT_FOUND` | automation_jobs | 404 | Target automation job does not exist or is not owned by user |
|
|
|
|
|
| `AUTOMATION_JOB_STORE_UNAVAILABLE` | automation_jobs | 503 | Automation job persistence unavailable |
|
|
|
|
|
| `NOT_FOUND` | runtime/tooling | 404 | Resource/tool target not found |
|
|
|
|
|
| `LOOKUP_FAILED` | runtime/tooling | 500 | Lookup or resolution failed |
|
|
|
|
|
| `INTERNAL_ERROR` | runtime/tooling | 500 | Internal execution error |
|
|
|
|
|
| `MISSING_RUNTIME_ARGS` | runtime/tooling | 400 | Required runtime arguments missing |
|
|
|
|
|
| `TOOL_PENDING_APPROVAL` | runtime/tooling | 409 | Tool call awaiting approval |
|
|
|
|
|
| `TOOL_REJECTED` | runtime/tooling | 403 | Tool call rejected by policy/user |
|
|
|
|
|
| `USER_STORE_UNAVAILABLE` | users | 503 | User storage or database access unavailable |
|
|
|
|
|
| `USER_NOT_FOUND` | users | 404 | Requested user profile not found |
|
|
|
|
|
| `USER_UPDATE_FIELDS_EMPTY` | users | 400 | Update request contains no writable fields |
|
|
|
|
|
| `USER_AVATAR_UNSUPPORTED_TYPE` | users | 422 | Avatar MIME type is unsupported |
|
|
|
|
|
| `USER_AVATAR_TOO_LARGE` | users | 413 | Avatar file size exceeds configured limit |
|
|
|
|
|
| `USER_AVATAR_EMPTY` | users | 422 | Avatar upload payload is empty |
|
|
|
|
|
| `USER_AVATAR_UPLOAD_FAILED` | users | 502 | Upstream storage upload failed |
|
|
|
|
|
| `USER_AUTH_LOOKUP_UNAVAILABLE` | users | 503 | Auth/identity phone lookup backend unavailable |
|
|
|
|
|
| `TODO_SERVICE_UNAVAILABLE` | todo | 503 | Todo persistence unavailable |
|
|
|
|
|
| `TODO_NOT_FOUND` | todo | 404 | Todo item does not exist |
|
|
|
|
|
| `TODO_ACCESS_FORBIDDEN` | todo | 403 | Current user cannot operate on target todo |
|
|
|
|
|
| `TODO_REORDER_DUPLICATE_ID` | todo | 400 | Reorder payload contains duplicate todo IDs |
|
|
|
|
|
| `TODO_STATUS_INVALID` | todo | 400 | Todo status filter value invalid |
|
|
|
|
|
| `TODO_PRIORITY_INVALID` | todo | 400 | Todo priority filter value out of range |
|
|
|
|
|
| `SCHEDULE_ITEM_INVALID_TIME_RANGE` | schedule_items | 400 | `end_at` must be after `start_at` |
|
|
|
|
|
| `SCHEDULE_ITEM_STORE_UNAVAILABLE` | schedule_items | 503 | Schedule item persistence unavailable |
|
|
|
|
|
| `SCHEDULE_ITEM_NOT_FOUND` | schedule_items | 404 | Schedule item does not exist |
|
|
|
|
|
| `SCHEDULE_ITEM_START_AT_TIMEZONE_REQUIRED` | schedule_items | 400 | `start_at` must include timezone when `end_at` is set |
|
|
|
|
|
| `SCHEDULE_ITEM_PAGE_INVALID` | schedule_items | 400 | Pagination `page` must be greater than or equal to 1 |
|
|
|
|
|
| `SCHEDULE_ITEM_PAGE_SIZE_INVALID` | schedule_items | 400 | Pagination `page_size` out of allowed range |
|
|
|
|
|
| `SCHEDULE_ITEM_SHARE_FORBIDDEN` | schedule_items | 403 | Current user cannot share this schedule item |
|
2026-03-30 11:37:41 +08:00
|
|
|
| `SCHEDULE_ITEM_SHARE_TARGET_NOT_FRIEND` | schedule_items | 403 | Recipient must be an accepted friend of current user |
|
2026-03-30 09:07:07 +08:00
|
|
|
| `SCHEDULE_ITEM_FORBIDDEN` | schedule_items | 403 | Current user does not have permission to edit this schedule item |
|
2026-03-27 14:05:14 +08:00
|
|
|
| `SCHEDULE_ITEM_SHARE_PERMISSION_EXCEEDED` | schedule_items | 403 | Requested share permission exceeds inviter permission |
|
|
|
|
|
| `SCHEDULE_ITEM_SUBSCRIPTION_ALREADY_ACTIVE` | schedule_items | 400 | Recipient already has active subscription |
|
|
|
|
|
| `SCHEDULE_ITEM_INVITE_ALREADY_SUBSCRIBED` | schedule_items | 400 | Recipient already accepted calendar invite |
|
|
|
|
|
| `SCHEDULE_ITEM_INVITE_ALREADY_PENDING` | schedule_items | 400 | Recipient already has pending calendar invite |
|
|
|
|
|
| `SCHEDULE_ITEM_AUTH_LOOKUP_UNAVAILABLE` | schedule_items | 503 | Auth/identity lookup unavailable when sharing |
|
2026-03-30 18:36:57 +08:00
|
|
|
| `SCHEDULE_ITEM_ACTOR_LOOKUP_UNAVAILABLE` | schedule_items | 503 | Actor profile lookup unavailable when constructing inbox change payload |
|
2026-03-27 14:05:14 +08:00
|
|
|
| `SCHEDULE_ITEM_PENDING_INVITE_NOT_FOUND` | schedule_items | 404 | No pending invitation exists for target item/user |
|
|
|
|
|
| `SCHEDULE_ITEM_ACCEPT_SUBSCRIPTION_FAILED` | schedule_items | 503 | Subscription accept flow failed unexpectedly |
|
|
|
|
|
| `SCHEDULE_ITEM_REJECT_SUBSCRIPTION_FAILED` | schedule_items | 503 | Subscription reject flow failed unexpectedly |
|
|
|
|
|
| `SCHEDULE_ITEM_DATETIME_TIMEZONE_REQUIRED` | schedule_items | 400 | Datetime input must include timezone |
|
|
|
|
|
| `SCHEDULE_ITEM_DATETIME_REQUIRED` | schedule_items | 400 | Required datetime input missing |
|
|
|
|
|
| `INBOX_MESSAGE_NOT_FOUND` | inbox_messages | 404 | Inbox message does not exist for current user |
|
|
|
|
|
| `INBOX_MESSAGE_STORE_UNAVAILABLE` | inbox_messages | 503 | Inbox message persistence unavailable |
|
2026-03-30 18:36:57 +08:00
|
|
|
| `INBOX_SSE_CONNECTION_LIMIT` | inbox_messages | 429 | SSE connections exceed per-user limit |
|
|
|
|
|
| `INBOX_INVALID_LAST_EVENT_ID` | inbox_messages | 422 | SSE Last-Event-ID format invalid |
|
|
|
|
|
| `INBOX_EVENT_STREAM_UNAVAILABLE` | inbox_messages | 503 | Inbox SSE stream read unavailable |
|
2026-03-27 14:05:14 +08:00
|
|
|
| `MEMORIES_USER_NOT_FOUND` | memories | 404 | User memory record does not exist |
|
|
|
|
|
| `MEMORIES_WORK_NOT_FOUND` | memories | 404 | Work memory record does not exist |
|
|
|
|
|
| `MEMORIES_SERVICE_UNAVAILABLE` | memories | 503 | Memories persistence unavailable |
|
|
|
|
|
| `FRIEND_REQUEST_SELF_NOT_ALLOWED` | friendships | 400 | User cannot send friend request to self |
|
|
|
|
|
| `FRIEND_ALREADY_ACCEPTED` | friendships | 400 | Users are already friends |
|
|
|
|
|
| `FRIEND_REQUEST_BLOCKED` | friendships | 400 | Friend request blocked by relationship status |
|
|
|
|
|
| `FRIEND_REQUEST_ALREADY_SENT` | friendships | 400 | Pending friend request already exists |
|
|
|
|
|
| `FRIENDSHIP_SERVICE_UNAVAILABLE` | friendships | 503 | Friendship persistence unavailable |
|
|
|
|
|
| `FRIEND_REQUEST_NOT_FOUND` | friendships | 404 | Friend request record not found |
|
|
|
|
|
| `FRIEND_REQUEST_FORBIDDEN` | friendships | 403 | Current user is not allowed for this friend request action |
|
|
|
|
|
| `FRIEND_REQUEST_NOT_PENDING` | friendships | 400 | Friend request is not in pending state |
|
|
|
|
|
| `FRIEND_INBOX_MESSAGE_NOT_FOUND` | friendships | 404 | Friend request inbox message not found |
|
|
|
|
|
| `FRIENDSHIP_DATA_INVALID` | friendships | 400 | Friendship record is missing required linkage fields |
|
|
|
|
|
| `FRIENDSHIP_NOT_FOUND` | friendships | 404 | Friendship record not found |
|
|
|
|
|
| `FRIENDSHIP_REMOVE_REQUIRES_ACCEPTED` | friendships | 400 | Only accepted friendships can be removed |
|
|
|
|
|
|
|
|
|
|
## Registry Coverage Check Script
|
|
|
|
|
|
|
|
|
|
Use the checker script to ensure this registry and frontend code mapping stay aligned:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python3 scripts/check_error_code_registry.py
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Optional arguments:
|
|
|
|
|
|
|
|
|
|
- `--doc`: custom registry markdown path
|
|
|
|
|
- `--mapper`: custom frontend mapper path (default: `apps/lib/core/network/error_code_mapper.dart`)
|
|
|
|
|
|
|
|
|
|
Output always includes three result groups:
|
|
|
|
|
|
|
|
|
|
- doc has code but frontend has no mapping
|
|
|
|
|
- frontend maps code but doc has no such code
|
|
|
|
|
- duplicate codes
|
|
|
|
|
|
|
|
|
|
Exit code policy:
|
|
|
|
|
|
|
|
|
|
- `0`: no inconsistency found
|
|
|
|
|
- non-`0`: at least one inconsistency found or input path invalid
|
|
|
|
|
|
|
|
|
|
## Agent Error Code Set
|
|
|
|
|
|
|
|
|
|
### Agent
|
|
|
|
|
|
|
|
|
|
- `AGENT_RUN_INPUT_INVALID`
|
|
|
|
|
- `AGENT_RUN_MESSAGES_INVALID`
|
2026-03-30 09:07:07 +08:00
|
|
|
- `AGENT_INVALID_RUN_ID`
|
2026-03-27 14:05:14 +08:00
|
|
|
- `AGENT_INVALID_LAST_EVENT_ID`
|
|
|
|
|
- `AGENT_SSE_CONNECTION_LIMIT`
|
|
|
|
|
- `AGENT_ATTACHMENT_EMPTY`
|
|
|
|
|
- `AGENT_ATTACHMENT_TOO_LARGE`
|
|
|
|
|
- `AGENT_AUDIO_UNSUPPORTED_FORMAT`
|
|
|
|
|
- `AGENT_AUDIO_TOO_LARGE`
|
|
|
|
|
- `AGENT_AUDIO_EMPTY`
|
|
|
|
|
- `AGENT_ASR_UNAVAILABLE`
|
|
|
|
|
- `AGENT_FORBIDDEN`
|
|
|
|
|
- `AGENT_PAYLOAD_INVALID`
|
|
|
|
|
- `AGENT_ATTACHMENTS_TOO_MANY`
|
|
|
|
|
- `AGENT_SIGNED_IMAGE_URL_INVALID`
|
|
|
|
|
- `AGENT_ATTACHMENT_STORAGE_UNAVAILABLE`
|
|
|
|
|
- `AGENT_ATTACHMENT_UNSUPPORTED_TYPE`
|
|
|
|
|
- `AGENT_ATTACHMENT_UPLOAD_FAILED`
|
|
|
|
|
- `AGENT_ATTACHMENT_BUCKET_INVALID`
|
|
|
|
|
- `AGENT_ATTACHMENT_PATH_SCOPE_INVALID`
|
|
|
|
|
- `AGENT_SIGNED_URL_GENERATION_FAILED`
|
|
|
|
|
- `AGENT_SESSION_ID_INVALID`
|
|
|
|
|
- `AGENT_SESSION_NOT_FOUND`
|
|
|
|
|
- `AGENT_USER_ID_INVALID`
|
|
|
|
|
|
|
|
|
|
## Compatibility Strategy
|
|
|
|
|
|
|
|
|
|
- Transition phase keeps `detail` and adds `code`/`params`.
|
|
|
|
|
- Frontend moves to code-first mapping first; backend can then continue migrating remaining endpoints.
|