Files

205 lines
13 KiB
Markdown

# 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 |
| `AGENT_INVALID_RUN_ID` | agent | 422 | SSE runId query invalid |
| `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 |
| `AGENT_UPSTREAM_CONNECTION_ERROR` | agent | 503 | Upstream AI service connection failed (network/proxy issue) |
| `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 |
| `ANALYTICS_LOGIN_PASSWORD_INVALID` | analytics | 401 | Analytics dashboard password is invalid |
| `ANALYTICS_AUTH_HEADER_MISSING` | analytics | 401 | Authorization header is missing when reading analytics data |
| `ANALYTICS_AUTH_SCHEME_INVALID` | analytics | 401 | Authorization scheme is invalid; Bearer token required |
| `ANALYTICS_AUTH_TOKEN_MISSING` | analytics | 401 | Bearer token is missing |
| `ANALYTICS_TOKEN_MALFORMED` | analytics | 401 | Analytics token format is malformed |
| `ANALYTICS_TOKEN_SIGNATURE_INVALID` | analytics | 401 | Analytics token signature verification failed |
| `ANALYTICS_TOKEN_PAYLOAD_INVALID` | analytics | 401 | Analytics token payload cannot be parsed |
| `ANALYTICS_TOKEN_EXPIRED` | analytics | 401 | Analytics token is expired |
| `ANALYTICS_DATE_FORMAT_INVALID` | analytics | 400 | Analytics date must use YYYY-MM-DD format |
| `ANALYTICS_FILE_NOT_FOUND` | analytics | 404 | Analytics day file does not exist |
| `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 |
| `SCHEDULE_ITEM_SHARE_TARGET_NOT_FRIEND` | schedule_items | 403 | Recipient must be an accepted friend of current user |
| `SCHEDULE_ITEM_FORBIDDEN` | schedule_items | 403 | Current user does not have permission to edit this schedule item |
| `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 |
| `SCHEDULE_ITEM_ACTOR_LOOKUP_UNAVAILABLE` | schedule_items | 503 | Actor profile lookup unavailable when constructing inbox change payload |
| `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 |
| `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 |
| `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
当前仓库未内置自动校验脚本,维护流程按以下约束执行:
- 更新本文件错误码时,同步检查前端映射文件:`apps/lib/data/network/error_code_mapper.dart`
- 任何新增/变更/废弃错误码必须在同一 PR 中完成「协议文档 + 前端映射 + 后端返回码」三方对齐
- 若后续补充自动校验脚本,需在本节追加命令与输出约定
## Agent Error Code Set
### Agent
- `AGENT_RUN_INPUT_INVALID`
- `AGENT_RUN_MESSAGES_INVALID`
- `AGENT_INVALID_RUN_ID`
- `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`
- `AGENT_UPSTREAM_CONNECTION_ERROR`
## Compatibility Strategy
- Transition phase keeps `detail` and adds `code`/`params`.
- Frontend moves to code-first mapping first; backend can then continue migrating remaining endpoints.