# 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 | | `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 | | `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_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 | | `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` - `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` ## Compatibility Strategy - Transition phase keeps `detail` and adds `code`/`params`. - Frontend moves to code-first mapping first; backend can then continue migrating remaining endpoints.