错误处理
错误响应格式
所有 API 错误遵循一致的 JSON 结构,并附带相应的 HTTP 状态码:
json
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description of what went wrong"
}
}code 字段是稳定的、机器可读的标识符,可在错误处理逻辑中使用。message 字段提供额外上下文,在同一错误码的不同出现场景中可能有所不同。
错误类别
认证错误 (401)
请求无法通过认证时返回。
| 错误码 | 描述 |
|---|---|
UNAUTHORIZED | Authorization 头缺失、格式错误或包含无效令牌。 |
KEY_EXPIRED | API 密钥有效但已超过过期日期。请创建新密钥继续使用。 |
json
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid Authorization header"
}
}授权错误 (403)
已认证的用户没有权限访问请求的资源时返回。
| 错误码 | 描述 |
|---|---|
FORBIDDEN | 您正在尝试访问或修改属于其他用户的资源(例如吊销他人的 API 密钥)。 |
未找到错误 (404)
请求的资源不存在时返回。
| 错误码 | 描述 |
|---|---|
NOT_FOUND | URL 中标识的资源(提取、API 密钥等)不存在或已被删除。 |
验证错误 (400)
请求体或参数无效时返回。
| 错误码 | 描述 |
|---|---|
VALIDATION_ERROR | 请求体未通过模式验证。message 字段描述了哪些字段无效。 |
MAX_KEYS_REACHED | 您已达到 10 个有效 API 密钥的上限。请先吊销现有密钥再创建新密钥。 |
json
{
"error": {
"code": "MAX_KEYS_REACHED",
"message": "Maximum of 10 active API keys allowed"
}
}速率限制错误 (429)
用量限额已超出时返回。
| 错误码 | 描述 |
|---|---|
USAGE_LIMIT_EXCEEDED | 您的月度提取限额已用完。请升级计划或等待下一个计费周期。 |
json
{
"error": {
"code": "USAGE_LIMIT_EXCEEDED",
"message": "Monthly extraction limit reached (25/25). Upgrade your plan for more extractions."
}
}服务器错误 (500)
服务端发生意外情况时返回。
| 错误码 | 描述 |
|---|---|
INTERNAL_ERROR | 发生意外的服务器错误。这些错误会被自动记录和调查。 |
EXTRACTION_FAILED | AI 处理管道在从文档提取数据时遇到错误。文档可能已损坏、不可读或格式不受支持。 |
可重试的错误
并非所有错误都应该重试。以下是详细分类:
| 错误码 | 可重试 | 操作 |
|---|---|---|
INTERNAL_ERROR | 是 | 使用指数退避重试 |
EXTRACTION_FAILED | 是 | 重试 -- AI 可能产生不同的结果。如果持续失败,请检查文档质量 |
USAGE_LIMIT_EXCEEDED | 否 | 升级计划或等待下一个计费周期 |
UNAUTHORIZED | 否 | 修复认证凭据 |
KEY_EXPIRED | 否 | 创建新的 API 密钥 |
FORBIDDEN | 否 | 您正在访问不属于自己的资源 |
NOT_FOUND | 否 | 资源不存在 -- 请验证 ID |
VALIDATION_ERROR | 否 | 修复请求体以匹配预期模式 |
MAX_KEYS_REACHED | 否 | 先吊销现有密钥再创建新密钥 |
重试策略
对于可重试的错误,使用指数退避以避免服务器过载。以下是一个可复用的实现:
typescript
interface ApiResponse<T> {
data?: T;
error?: { code: string; message: string };
}
async function fetchWithRetry<T>(
url: string,
options: RequestInit,
maxRetries = 3,
): Promise<ApiResponse<T>> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
const body = await response.json();
if (response.ok) {
return body as ApiResponse<T>;
}
const errorCode = body?.error?.code;
// Only retry server errors
if (
(errorCode === "INTERNAL_ERROR" || errorCode === "EXTRACTION_FAILED") &&
attempt < maxRetries
) {
const delayMs = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s
console.log(`Retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
// Non-retryable error -- return immediately
return body as ApiResponse<T>;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
const delayMs = 1000 * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
}
}
throw lastError ?? new Error("Max retries exceeded");
}用法:
typescript
const result = await fetchWithRetry<ExtractionResult>(
"https://api.docmap.io/v1/extractions/run",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: "tmpl_abc123",
fileName: "invoice.pdf",
pdfBase64: encodedPdf,
mimeType: "application/pdf",
}),
},
);
if (result.error) {
console.error(`Extraction failed: [${result.error.code}] ${result.error.message}`);
} else {
console.log("Extracted data:", result.data);
}最佳实践
始终先检查 HTTP 状态码。
2xx状态码表示成功;其他均为错误。不要在未检查的情况下假设响应体包含data字段。解析错误体中的
code字段。 在控制流中使用code(而非message)。错误消息可能随时间变化,但错误码是稳定的。处理特定错误码。 对已知的错误码如
USAGE_LIMIT_EXCEEDED或KEY_EXPIRED进行分支处理,向用户显示针对性消息或触发特定工作流。记录未知错误。 如果遇到未明确处理的错误码,请记录完整响应(状态码、错误码和消息)以便调试。
不要重试 4xx 错误。 认证、授权、验证和未找到错误不会自行解决。在重试前修复根本问题。
设置重试限制。 切勿无限重试。对于服务器错误,三次重试加指数退避(1 秒、2 秒、4 秒)是合理的默认值。
