accounts/docs/api/errors.md
2026-04-14 16:32:15 +08:00

354 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Error Conventions / 错误约定
## 中文
### 通用原则
当前 API 没有单独的全局 error schema package而是由几类 handler 约定共同形成:
1. 大多数业务 handler 使用 `respondError`,返回统一 envelope。
2. 一部分 internal / agent / GORM 简单 handler 直接 `c.JSON`,只返回 `error``error + message`
3. 因此文档层面必须区分“稳定错误结构”和“简化错误结构”,不能假设所有接口完全一致。
### 标准错误 envelope
大多数认证、账户、管理员、XWorkmate、计费接口的失败响应是
```json
{"error":"code","message":"human readable message"}
```
字段含义:
| 字段 | 含义 |
| --- | --- |
| `error` | 机器可读错误码,通常稳定,适合前端做分支。 |
| `message` | 面向人类的错误描述。 |
### 扩展错误 envelope
少数接口会在标准形状上附加字段,例如:
| 场景 | 额外字段 |
| --- | --- |
| MFA 锁定 | `retryAt`、`mfaToken` |
| Admin settings 乐观并发冲突 | `version`、`matrix` |
| Sandbox binding / GORM 错误 | 有时直接返回底层 `err.Error()` 作为 `message` |
| `/api/ping` | 不走错误 envelope是纯成功读取接口 |
### 简化错误 envelope
以下 handler 家族经常直接返回简化结构:
```json
{"error":"some_code"}
```
```json
{"error":"some_code","message":"..."}
```
典型来源:
| 文件 | 典型接口 |
| --- | --- |
| `api/agent_server.go` | `/api/agent-server/v1/users`、`/status` |
| `api/user_agents.go` | `/api/agent-server/v1/nodes`、legacy `/api/agent/nodes` |
| `api/internal_sandbox_guest.go` | `/api/internal/sandbox/guest` |
| `api/admin_sandbox.go` | `/api/auth/admin/sandbox/*`、`/api/admin/sandbox/*` |
| `internal/auth/middleware.go` | JWT / internal-service middleware 直接中断请求时 |
### 错误来源分层
| 来源层 | 典型错误码 / 文本 | 说明 |
| --- | --- | --- |
| 请求绑定与参数校验 | `invalid_request`、`missing_credentials`、`invalid_email`、`password_too_short` | JSON body 缺字段、query 中带敏感参数、格式不对。 |
| Session / JWT / internal token | `session_token_required`、`invalid_session`、`missing authorization header`、`invalid or expired token`、`missing service token` | 认证前置失败。 |
| 角色 / 权限 / 账户状态 | `forbidden`、`root_only`、`account_suspended`、`read_only_account`、`root_email_enforced` | 用户存在,但当前身份不允许执行操作。 |
| 业务状态 | `email_already_exists`、`subscription_not_found`、`mfa_not_enabled`、`policy_not_found` | 领域对象状态不满足当前请求。 |
| 外部系统或后台依赖 | `verification_failed`、`xworkmate_secret_write_failed`、`stripe_cancel_failed`、`collector_status_unavailable` | SMTP、Vault、Stripe、DB、Xray render 等依赖失败。 |
### 高频错误码索引
#### 认证与会话
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `credentials_in_query` | `400` | `api/api.go` | 登录或敏感接口不允许把凭据放在 query 中。 |
| `missing_credentials` | `400` | `api/api.go` | 登录、注册缺少关键字段。 |
| `user_not_found` | `404` | `api/api.go`、`api/user_agents.go` | 通过 identifier 或 session userID 找不到用户。 |
| `invalid_credentials` | `401` | `api/api.go` | 密码校验失败。 |
| `email_not_verified` | `401` | `api/api.go` | 邮箱尚未验证,禁止登录或 OAuth 登录。 |
| `session_token_required` | `401` | `api/api.go`、`api/xworkmate.go`、`api/admin_users_metrics.go` | 需要 session token。 |
| `invalid_session` | `401` | 多个 session-based handler | session 不存在、过期或无法匹配。 |
| `session_user_lookup_failed` | `500` | 多个 handler | session 存在,但无法回查用户。 |
#### MFA
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `mfa_ticket_required` | `400` | `verifyMFALogin` | 登录挑战完成接口缺少 ticket。 |
| `mfa_code_required` | `400` | `verifyMFALogin`、`verifyTOTP` | 缺少 TOTP 验证码。 |
| `invalid_mfa_ticket` | `401` | `verifyMFALogin` | 登录用 MFA challenge 已失效。 |
| `invalid_mfa_token` | `401` | `provisionTOTP`、`verifyTOTP` | TOTP provisioning / verify 的 token 不存在或过期。 |
| `invalid_mfa_code` | `401` / `500` | MFA handlers | 验证码错误,或库校验过程失败。 |
| `mfa_challenge_locked` | `429` | `verifyTOTP` | 连续错误次数过多,被暂时锁定。 |
| `mfa_not_enabled` | `400` | `verifyMFALogin`、`disableMFA` | 用户当前没有启用 MFA。 |
#### 注册、邮箱验证、密码重置
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `verification_required` | `400` | `register` | 已启用邮箱验证,但未提供验证码。 |
| `invalid_code` | `400` | `register`、`verifyEmail` | 验证码错误或过期。 |
| `verification_failed` | `500` | 邮件验证流程 | SMTP 发送或用户更新失败。 |
| `email_already_exists` | `409` | `register`、`sendEmailVerification` | 邮箱已存在或已验证。 |
| `name_already_exists` | `409` | `register` | 用户名冲突。 |
| `password_reset_failed` | `500` | password reset flows | 密码重置邮件发送、用户更新或其他内部步骤失败。 |
| `invalid_token` | `400` | `confirmPasswordReset` | reset token 无效或过期。 |
#### OAuth / token exchange / refresh
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `provider_not_found` | `404` | OAuth routes | provider 未注册。 |
| `code_missing` | `400` | `oauthCallback` | callback query 中缺少 `code`。 |
| `oauth_exchange_failed` | `500` | `oauthCallback` | 与 provider 交换 token 失败。 |
| `fetch_profile_failed` | `500` | `oauthCallback` | 拉取 provider profile 失败。 |
| `invalid_exchange_code` | `401` | `exchangeToken` | 一次性 exchange code 无效或已过期。 |
| `token_service_unavailable` | `503` | `refreshToken` | 当前未启用 token service。 |
| `invalid_refresh_token` | `401` | `refreshToken` | refresh token 无效或已过期。 |
#### 管理员与权限
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `forbidden` | `403` | `requireAdminPermission`、`RequireRole` | 用户权限不足。 |
| `root_only` | `403` | sandbox bind / assume / tenant bootstrap | 只有 root 可执行。 |
| `root_email_enforced` | `403` | `requireAdminPermission` | root role 被限制给 `admin@svc.plus`。 |
| `metrics_unavailable` | `503` / 其他 | `adminUsersMetrics` | 指标 provider 未配置或执行失败。 |
| `read_only_account` | `403` | 多个写接口 | demo/read-only 账号禁止写操作。 |
| `account_suspended` | `403` | session user checks | 账号被暂停。 |
#### XWorkmate / Vault
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `xworkmate_vault_unavailable` | `503` | `ensureXWorkmateVaultService` | 当前未注入 Vault backend。 |
| `tenant_membership_required` | `403` | `xworkmate.go` | 当前用户没有 tenant membership。 |
| `tenant_not_found` | `404` | `xworkmate.go` | host 解析出的 tenant 不存在。 |
| `xworkmate_profile_forbidden` | `403` | `updateXWorkmateProfile` | 无权修改 tenant integration profile。 |
| `token_persistence_forbidden` | `400` | `updateXWorkmateProfile` | 禁止把 raw token/password/api key 直接落库。 |
| `xworkmate_secret_unknown_target` | `400` | secret PUT/DELETE | `:target` 不在允许列表内。 |
| `xworkmate_secret_write_failed` | `500` | secret PUT | 写入 Vault/backend 失败。 |
| `xworkmate_secret_delete_failed` | `500` | secret DELETE | 删除 Vault/backend secret 失败。 |
#### 计费、订阅、流量与调度
| 错误码 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `subscription_not_found` | `404` | `cancelSubscription` | 订阅不存在。 |
| `subscription_upsert_failed` | `500` | `upsertSubscription` | 订阅落库失败。 |
| `stripe_cancel_failed` | `502` | `cancelSubscription` | 调用 Stripe 取消失败。 |
| `usage_summary_unavailable` | `500` | `accountUsageSummary` | 读取使用量汇总失败。 |
| `invalid_start` / `invalid_end` | `400` | `accountUsageBuckets` | 时间范围参数不是 RFC3339。 |
| `policy_not_found` | `404` | `accountPolicy`、`internalAccountPolicy` | 账户策略快照不存在。 |
| `collector_status_unavailable` | `500` | `adminCollectorStatus` | collector 读面不可用。 |
| `scheduler_status_unavailable` | `500` | `adminSchedulerStatus` | 调度决策读面不可用。 |
#### Agent 与内部服务
| 错误码 / 文本 | 常见状态码 | 来源 | 含义 |
| --- | --- | --- | --- |
| `missing_token` | `401` | `api/agent_server.go` | agent token 缺失。 |
| `invalid_token` | `401` | `api/agent_server.go` | agent token 无效。 |
| `agent_registry_unavailable` | `503` | `api/agent_server.go` | 未注入 `agentserver.Registry`。 |
| `list_users_failed` | `500` | `internalPublicOverview`、`listAgentUsers`、`internalNetworkIdentities` | 枚举用户失败。 |
| `store_not_configured` / `store_unavailable` | `503` | internal handlers | store 未配置。 |
| `sandbox_missing` | `404` | sandbox guest handlers | sandbox 用户不存在。 |
### 调用方建议
1.`respondError` 路由,优先按 `error` 做程序分支,`message` 仅用于展示。
2. 对 agent/internal 路由,不要假设一定有 `message`
3.`429 mfa_challenge_locked`,调用方应读取 `retryAt` 决定 UI 倒计时。
4.`POST /api/auth/admin/settings``409`,应读取返回的 `version``matrix` 重新加载服务端最新值。
## English
### Core Rule
The current API does not have a single shared error-schema package. Instead, error behavior is shaped by a few implementation conventions:
1. Most business handlers use `respondError`, which produces the standard envelope.
2. Some internal / agent / simple GORM-backed handlers call `c.JSON` directly and return only `error` or `error + message`.
3. Documentation therefore has to distinguish between the stable error envelope and simplified handler-local errors.
### Standard Error Envelope
Most authentication, account, admin, XWorkmate, and accounting handlers return:
```json
{"error":"code","message":"human readable message"}
```
Field meaning:
| Field | Meaning |
| --- | --- |
| `error` | Machine-readable error code that is usually stable enough for frontend branching. |
| `message` | Human-readable explanation. |
### Extended Error Envelope
Some handlers add extra fields on top of the standard shape:
| Scenario | Extra fields |
| --- | --- |
| MFA lockout | `retryAt`, `mfaToken` |
| Admin settings optimistic conflict | `version`, `matrix` |
| Sandbox binding / GORM errors | Sometimes the raw `err.Error()` is surfaced in `message` |
| `/api/ping` | Not an error-envelope route; it is a pure success read |
### Simplified Error Envelope
The following handler families commonly return:
```json
{"error":"some_code"}
```
or:
```json
{"error":"some_code","message":"..."}
```
Typical sources:
| File | Example APIs |
| --- | --- |
| `api/agent_server.go` | `/api/agent-server/v1/users`, `/status` |
| `api/user_agents.go` | `/api/agent-server/v1/nodes`, legacy `/api/agent/nodes` |
| `api/internal_sandbox_guest.go` | `/api/internal/sandbox/guest` |
| `api/admin_sandbox.go` | `/api/auth/admin/sandbox/*`, `/api/admin/sandbox/*` |
| `internal/auth/middleware.go` | Direct JWT / internal-service middleware rejection responses |
### Error Sources By Layer
| Source layer | Typical codes / texts | Meaning |
| --- | --- | --- |
| Request binding and validation | `invalid_request`, `missing_credentials`, `invalid_email`, `password_too_short` | Bad JSON bodies, forbidden query-string credentials, invalid formats. |
| Session / JWT / internal token | `session_token_required`, `invalid_session`, `missing authorization header`, `invalid or expired token`, `missing service token` | Authentication precondition failed. |
| Role / permission / account-state checks | `forbidden`, `root_only`, `account_suspended`, `read_only_account`, `root_email_enforced` | The user exists but is not allowed to perform the action. |
| Business-state failures | `email_already_exists`, `subscription_not_found`, `mfa_not_enabled`, `policy_not_found` | Domain state does not satisfy the requested transition. |
| External or backend dependency failures | `verification_failed`, `xworkmate_secret_write_failed`, `stripe_cancel_failed`, `collector_status_unavailable` | SMTP, Vault, Stripe, DB, Xray rendering, and similar dependency failures. |
### High-Frequency Error Index
#### Authentication And Sessions
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `credentials_in_query` | `400` | `api/api.go` | Sensitive credentials were sent through the query string. |
| `missing_credentials` | `400` | `api/api.go` | Required login or registration fields are missing. |
| `user_not_found` | `404` | `api/api.go`, `api/user_agents.go` | No user matches the identifier or session-derived user ID. |
| `invalid_credentials` | `401` | `api/api.go` | Password verification failed. |
| `email_not_verified` | `401` | `api/api.go` | The email is not verified, so login is blocked. |
| `session_token_required` | `401` | `api/api.go`, `api/xworkmate.go`, `api/admin_users_metrics.go` | A session token is required. |
| `invalid_session` | `401` | Multiple session-based handlers | The session does not exist, is expired, or cannot be matched. |
| `session_user_lookup_failed` | `500` | Multiple handlers | The session exists but the backing user cannot be loaded. |
#### MFA
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `mfa_ticket_required` | `400` | `verifyMFALogin` | The MFA completion endpoint is missing its ticket. |
| `mfa_code_required` | `400` | `verifyMFALogin`, `verifyTOTP` | No TOTP code was supplied. |
| `invalid_mfa_ticket` | `401` | `verifyMFALogin` | The login MFA challenge has expired or is invalid. |
| `invalid_mfa_token` | `401` | `provisionTOTP`, `verifyTOTP` | The provisioning / verification token does not exist or has expired. |
| `invalid_mfa_code` | `401` / `500` | MFA handlers | The code is wrong, or the verification library errored. |
| `mfa_challenge_locked` | `429` | `verifyTOTP` | Too many invalid attempts caused a temporary lockout. |
| `mfa_not_enabled` | `400` | `verifyMFALogin`, `disableMFA` | The user does not currently have MFA enabled. |
#### Registration, Email Verification, And Password Reset
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `verification_required` | `400` | `register` | Email verification is enabled but the code was not provided. |
| `invalid_code` | `400` | `register`, `verifyEmail` | The verification code is wrong or expired. |
| `verification_failed` | `500` | Email-verification flows | SMTP delivery or user-update work failed. |
| `email_already_exists` | `409` | `register`, `sendEmailVerification` | The email already exists or is already verified. |
| `name_already_exists` | `409` | `register` | Username conflict. |
| `password_reset_failed` | `500` | Password-reset flows | Email delivery, user update, or other internal reset steps failed. |
| `invalid_token` | `400` | `confirmPasswordReset` | The reset token is invalid or expired. |
#### OAuth, Token Exchange, And Refresh
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `provider_not_found` | `404` | OAuth routes | The provider is not registered. |
| `code_missing` | `400` | `oauthCallback` | The callback query does not contain `code`. |
| `oauth_exchange_failed` | `500` | `oauthCallback` | Exchanging the provider code failed. |
| `fetch_profile_failed` | `500` | `oauthCallback` | Fetching the provider profile failed. |
| `invalid_exchange_code` | `401` | `exchangeToken` | The one-time exchange code is invalid or expired. |
| `token_service_unavailable` | `503` | `refreshToken` | The token service is not enabled. |
| `invalid_refresh_token` | `401` | `refreshToken` | The refresh token is invalid or expired. |
#### Admin And Permission Errors
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `forbidden` | `403` | `requireAdminPermission`, `RequireRole` | The caller lacks the required permission. |
| `root_only` | `403` | Sandbox bind / assume / tenant bootstrap flows | Only the root user may perform the action. |
| `root_email_enforced` | `403` | `requireAdminPermission` | The root role is restricted to `admin@svc.plus`. |
| `metrics_unavailable` | `503` and others | `adminUsersMetrics` | The metrics provider is missing or failed. |
| `read_only_account` | `403` | Multiple write handlers | Demo/read-only accounts are blocked from writes. |
| `account_suspended` | `403` | Session user checks | The account has been suspended. |
#### XWorkmate And Vault
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `xworkmate_vault_unavailable` | `503` | `ensureXWorkmateVaultService` | No Vault backend has been injected. |
| `tenant_membership_required` | `403` | `xworkmate.go` | The caller has no tenant membership. |
| `tenant_not_found` | `404` | `xworkmate.go` | The host-resolved tenant does not exist. |
| `xworkmate_profile_forbidden` | `403` | `updateXWorkmateProfile` | The caller may not update the tenant integration profile. |
| `token_persistence_forbidden` | `400` | `updateXWorkmateProfile` | Raw token/password/api-key values may not be persisted directly. |
| `xworkmate_secret_unknown_target` | `400` | Secret PUT/DELETE handlers | The `:target` path value is outside the allowed set. |
| `xworkmate_secret_write_failed` | `500` | Secret PUT | Persisting the Vault/backend secret failed. |
| `xworkmate_secret_delete_failed` | `500` | Secret DELETE | Deleting the Vault/backend secret failed. |
#### Billing, Subscription, Traffic, And Scheduler Reads
| Code | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `subscription_not_found` | `404` | `cancelSubscription` | The subscription does not exist. |
| `subscription_upsert_failed` | `500` | `upsertSubscription` | Persisting the subscription failed. |
| `stripe_cancel_failed` | `502` | `cancelSubscription` | Stripe cancellation failed. |
| `usage_summary_unavailable` | `500` | `accountUsageSummary` | Usage summary reads failed. |
| `invalid_start` / `invalid_end` | `400` | `accountUsageBuckets` | The time-range query parameters are not valid RFC3339 timestamps. |
| `policy_not_found` | `404` | `accountPolicy`, `internalAccountPolicy` | No account policy snapshot is available. |
| `collector_status_unavailable` | `500` | `adminCollectorStatus` | Collector read models are unavailable. |
| `scheduler_status_unavailable` | `500` | `adminSchedulerStatus` | Scheduler decision reads are unavailable. |
#### Agent And Internal-Service Errors
| Code / text | Typical status | Source | Meaning |
| --- | --- | --- | --- |
| `missing_token` | `401` | `api/agent_server.go` | The agent token is missing. |
| `invalid_token` | `401` | `api/agent_server.go` | The agent token is invalid. |
| `agent_registry_unavailable` | `503` | `api/agent_server.go` | No `agentserver.Registry` has been injected. |
| `list_users_failed` | `500` | `internalPublicOverview`, `listAgentUsers`, `internalNetworkIdentities` | Enumerating users failed. |
| `store_not_configured` / `store_unavailable` | `503` | Internal handlers | The store has not been configured. |
| `sandbox_missing` | `404` | Sandbox guest handlers | The sandbox user does not exist. |
### Client Guidance
1. For `respondError`-based routes, branch on `error` first and treat `message` as display text.
2. For agent/internal routes, do not assume `message` is always present.
3. For `429 mfa_challenge_locked`, read `retryAt` and drive the UI countdown from it.
4. For `409` from `POST /api/auth/admin/settings`, reload the server-returned `version` and `matrix` before retrying.