12 KiB
12 KiB
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.
{
"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 placeholdersdetail(required by RFC7807): human-readable fallback/debug text
Backend Rules
- Do not rely on free-text
detailas 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-> fallbackdetail. - User-facing text should come from local l10n mapping by
code. - Unknown code fallback:
- status-based generic localized message
- 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 |
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 Script
Use the checker script to ensure this registry and frontend code mapping stay aligned:
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_INVALIDAGENT_RUN_MESSAGES_INVALIDAGENT_INVALID_RUN_IDAGENT_INVALID_LAST_EVENT_IDAGENT_SSE_CONNECTION_LIMITAGENT_ATTACHMENT_EMPTYAGENT_ATTACHMENT_TOO_LARGEAGENT_AUDIO_UNSUPPORTED_FORMATAGENT_AUDIO_TOO_LARGEAGENT_AUDIO_EMPTYAGENT_ASR_UNAVAILABLEAGENT_FORBIDDENAGENT_PAYLOAD_INVALIDAGENT_ATTACHMENTS_TOO_MANYAGENT_SIGNED_IMAGE_URL_INVALIDAGENT_ATTACHMENT_STORAGE_UNAVAILABLEAGENT_ATTACHMENT_UNSUPPORTED_TYPEAGENT_ATTACHMENT_UPLOAD_FAILEDAGENT_ATTACHMENT_BUCKET_INVALIDAGENT_ATTACHMENT_PATH_SCOPE_INVALIDAGENT_SIGNED_URL_GENERATION_FAILEDAGENT_SESSION_ID_INVALIDAGENT_SESSION_NOT_FOUNDAGENT_USER_ID_INVALIDAGENT_UPSTREAM_CONNECTION_ERROR
Compatibility Strategy
- Transition phase keeps
detailand addscode/params. - Frontend moves to code-first mapping first; backend can then continue migrating remaining endpoints.