Skip to content

Api Error Handling

worable edited this page Oct 12, 2026 · 1 revision

错误处理

StackPanel API 统一使用 RFC 7807 / RFC 9457 problem details,Content-Type 为 application/problem+json。

错误响应格式

所有错误响应都是 JSON problem details:

{
  "type": "https://errors.stackpanel.dev/auth/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "没有权限执行此操作",
  "instance": "/admin/users",
  "code": "auth.forbidden",
  "requestId": "req-8",
  "errors": [{ "field": "email", "code": "validation.invalid_format", "message": "Invalid email" }]
}
字段 说明
type 由 code 派生的 URI:https://errors.stackpanel.dev/ 加 code 中 . 替换为 /
title 中性英文标题(按状态码)
status HTTP 状态码,与响应一致
detail 可读说明;5xx 不泄露内部信息
instance 产生错误的请求路径
code 稳定契约,命名空间化(如 auth.forbidden、sp_v1.nonce_replayed)
requestId 关联 ID,同时写入 API 日志
errors 字段级校验失败,仅校验错误才出现

内核 onSend 会把遗留的 { "error": "..." } 响应体提升为 detail,但不再输出 error 成员。调用方应读取 code(机器判断)与 detail / title(展示)。

状态码

状态码 含义 触发场景
400 请求无效 路由内手工校验失败、参数格式错误
401 未认证 缺少或无效的会话/Token/签名
403 禁止访问 权限或 scope 不足
404 未找到 资源不存在、插件未激活、非回环访问 /metrics
405 方法不允许 /mcp 的 GET 等不支持的传输方法
409 冲突 资源已存在、依赖冲突、路由冲突
410 已移除 资源已下线
413 请求体过大 文件上传或签名体超过限制
415 媒体类型不支持 上传类型不受支持
422 实体不可处理 schema 校验失败、ZIP/签名校验失败
429 请求过多 限流 / 配额 / 渠道 RPM
500 服务器错误 未捕获的异常
501 未实现 功能未配置,如密钥存储未启用、缺少 pg 工具
503 服务不可用 依赖不可达、导入维护模式

未匹配路由由 setNotFoundHandler 返回 404 request.not_found。

错误处理器

内核 setErrorHandler(apps/api/src/app.ts)的处理顺序:

error →
  if isPluginError(error):        # 插件或内核显式抛出,品牌检查而非 instanceof
    return buildProblem({ status, code, detail, errors }) + status
  else if 是 schema 校验错误 (validation 数组):
    return problem(code="validation.invalid", detail="请求参数无效") + 422
  else if statusCode 是 4xx:
    return problem(detail=error.message ?? "无效请求") + statusCode
  else:
    log error
    return problem(code="internal.unexpected", detail="服务器内部错误") + 500

PluginError 属于预期结果,以 info 级别记录;其余错误按 error 级别入日志。5xx 的 详细信息只入日志,不返回给客户端。

常见错误场景

认证错误

// 401 auth.unauthenticated
{ "code": "auth.unauthenticated", "status": 401, "detail": "未登录或会话已过期" }
// 401 auth.invalid_credentials
{ "code": "auth.invalid_credentials", "status": 401, "detail": "邮箱或密码错误" }

权限错误

// 403 auth.forbidden
{ "code": "auth.forbidden", "status": 403, "detail": "没有权限执行此操作" }

权限不足时会同时写入审计日志,action 为 auth.forbidden。

开放平台 / 渠道错误

// 429 request.rate_limited(每 Token RPM)
{ "code": "request.rate_limited", "status": 429, "detail": "请求过于频繁,请稍后再试" }
// 429 request.concurrent_request(每 Token 并发)
{ "code": "request.concurrent_request", "status": 429, "detail": "并发请求超出限制,请稍后再试" }
// 401 sp_v1.signature_invalid / sp_v1.nonce_replayed / sp_v1.signature_expired
{ "code": "sp_v1.signature_invalid", "status": 401, "detail": "请求签名无效" }
// 403 sp_v1.scope_forbidden
{ "code": "sp_v1.scope_forbidden", "status": 403, "detail": "缺少能力授权:order:write" }

插件错误

// 404 request.not_found(插件未激活或路由不存在)
{ "code": "request.not_found", "status": 404, "detail": "接口不存在" }
// 409 request.conflict(依赖冲突)
{ "code": "request.conflict", "status": 409, "detail": "插件 store 被已启用的插件依赖:my-plugin" }

上传错误

// 422 validation.invalid
{ "code": "validation.invalid", "status": 422, "detail": "插件包包含源码文件:src/index.ts" }
{ "code": "validation.invalid", "status": 422, "detail": "包签名无效" }
// 413 request.payload_too_large
{ "code": "request.payload_too_large", "status": 413, "detail": "文件过大或不是有效的 multipart 表单" }

SDK 错误处理

使用 @stackpanel/sdk 的 ApiClient 时,错误以 ApiError 抛出:

import { ApiClient, ApiError } from '@stackpanel/sdk';

try {
  const result = await api.login(email, password);
} catch (err) {
  if (err instanceof ApiError) {
    console.log(err.status); // HTTP 状态码
    console.log(err.code); // 稳定错误码(problem `code` / timeout / network / circuit_open)
    console.log(err.message); // detail ?? title
    console.log(err.details); // 原始响应体
    console.log(err.requestId); // 关联 ID
    console.log(err.errors); // 字段级校验错误
  }
}

ApiError 属性

属性 说明
status HTTP 状态码(网络错误为 0)
code problem code;传输层错误为 timeout / network / circuit_open / unknown
message detail 优先,其次 title
details 原始响应体 JSON(如有)
requestId problem requestId(如有)
errors problem errors[](如有)

返回类型与完整属性定义见 SDK 客户端。

Clone this wiki locally