本页内容
错误处理
一致地处理 Merchant API 错误:响应封装、HTTP 状态分组、机器可读错误码、请求限制与重试指引。
API 使用标准 HTTP 状态码。成功响应直接返回资源体。错误遵循 RFC 9457 Problem Details for HTTP APIs(application/problem+json),并带两个扩展成员:用于程序化判断的顶层 code,以及单字段校验错误上的顶层 field(snake_case)。当多个字段同时出错时,field 被 errors[] 数组替代(Paymos 扩展成员,§3.2),承载逐字段的明细。
程序化处理和本地化时判断 code——绝不要判断 title 或 detail 文本。字符串字段可能在版本之间改写;code 才是稳定的契约。
{
"type": "https://paymos.io/docs/errors/codes#insufficient_balance",
"title": "Conflict",
"status": 409,
"detail": "Merchant balance is insufficient.",
"code": "insufficient_balance"
}
每个错误响应都遵循 RFC 9457 Problem Details for HTTP APIs,以 Content-Type: application/problem+json 返回。
单错误(多数响应)
{
"type": "https://paymos.io/docs/errors/codes#insufficient_balance",
"title": "Conflict",
"status": 409,
"detail": "Merchant balance is insufficient.",
"code": "insufficient_balance"
}
多字段校验
仅在多个字段同时校验失败时发出。顶层 code 恒为 validation_failed;逐字段明细在 errors[] 中:
{
"type": "https://paymos.io/docs/errors/codes#validation_failed",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed.",
"code": "validation_failed",
"errors": [
{ "code": "field_required", "field": "currency", "message": "Currency is required." },
{ "code": "field_must_be_positive", "field": "amount", "message": "Amount must be > 0." }
]
}
字段参考
| 字段 | 类型 | RFC | 说明 |
|---|---|---|---|
type |
string (URI) | RFC 9457 §3.1.1 | 指向具体 code 文档行的深链。每个错误码稳定不变——发布后永不更改。 |
title |
string | RFC 9457 §3.1.3 | 简短的 HTTP 状态标题("Bad Request"、"Conflict"……)。 |
status |
integer | RFC 9457 §3.1.2 | HTTP 状态码(与响应状态一致)。 |
detail |
string | RFC 9457 §3.1.4 | 本次具体错误的可读英文说明。 |
code |
string | 扩展(§3.2) | 稳定的机器可读标识符——见错误码。集成中以此为准做判断。 |
field |
string | 扩展(§3.2) | 单字段校验错误的线上字段名(snake_case)。非字段错误时整个键省略——JSON 中不存在该键,而不是 null。仅在 errors[] 不存在时出现。 |
errors |
array | 扩展(§3.2) | 仅在多个字段同时出错时出现。每个条目为 { code, field, message }。 |
为什么要结构化错误码(而不只是 message)
message 和 detail 文本可能在版本之间演进、被本地化或补充上下文。code 是稳定的契约——一个错误码发布后,其语义永不改变。程序化判断和本地化都基于 code。
import { RateLimitError, ValidationError } from '@paymos/sdk';
try {
await paymos.withdrawals.create(request);
} catch (error) {
if (error instanceof ValidationError) {
for (const item of error.errors) highlight(item.field, item.message);
} else if (error instanceof RateLimitError) {
scheduleRetry(error.retryAfterSeconds);
}
throw error;
}
use paymos::{ApiErrorKind, Error};
match paymos.withdrawals().create(&request).await {
Ok(withdrawal) => process(withdrawal),
Err(Error::Api(error)) if error.kind == ApiErrorKind::Validation => {
for item in &error.errors {
highlight(item.field.as_deref(), &item.message);
}
}
Err(Error::Api(error)) if error.kind == ApiErrorKind::RateLimit => {
schedule_retry(error.retry_after);
}
Err(error) => return Err(error),
}
错误类型
每个状态都随错误封装返回,带 title 和机器可读的 code。一个状态可能覆盖多种场景——按 code 分支,而不是按状态码。
| HTTP 状态码 | title |
说明 |
|---|---|---|
400 |
Bad Request |
输入校验失败或请求体无效。单字段失败在顶层携带 code + field;多字段失败填充 errors[]。 |
401 |
Unauthorized |
HMAC 凭证缺失、格式错误、过期或无效。具体原因见 code(invalid_credentials、timestamp_expired、authorization_malformed 等)。 |
403 |
Forbidden |
已认证,但密钥类型、环境或作用域不允许该操作。具体原因见 code(merchant_suspended、whitelist_required 等)。 |
404 |
Not Found |
资源不存在、对调用方不可见(属于其他商户的资源返回 not_found,绝不返回 forbidden),或没有端点匹配请求路径(route_not_found)。 |
405 |
Method Not Allowed |
路径存在,但不支持此 HTTP 方法。携带 method_not_allowed 错误码。 |
409 |
Conflict |
请求与当前状态冲突——非法状态迁移、资源已存在,或某个守卫阻止了操作。具体原因见 code(insufficient_balance、withdrawal_quota_exceeded、invoice_cannot_be_cancelled 等)。 |
410 |
Gone |
资源不再可用(例如已过期或已取消的账单)。 |
413 |
Payload Too Large |
请求体超过大小限制(1 MiB)。携带 payload_too_large 错误码。 |
415 |
Unsupported Media Type |
Content-Type 不是 application/json。携带 unsupported_media_type 错误码。 |
429 |
Too Many Requests |
超过按商户计算的限流。重试前读取 Retry-After 请求头(秒)。 |
500 |
Internal Server Error |
我们这边出了故障。以指数退避重试。 |
503 |
Service Unavailable |
临时依赖或处理故障(exchange_rate_unavailable、acceptance_disabled、outbound_frozen 等)。以退避方式重试。 |
客户端逻辑始终由 code(而非 HTTP 状态)驱动——多个不同的业务场景共用一个 HTTP 状态,线上错误码用来区分它们。
限流
API 默认允许每个商户每秒 30 次请求,POST /v1/invoices 有更严格的限制(5 次/秒)。限流按商户、按一秒窗口计数——你持有的每个 API 密钥共用同一份预算——两者都可按商户配置。超过限制时会收到 429 Too Many Requests 响应,并携带 Retry-After: 1 请求头。
{
"type": "https://paymos.io/docs/errors/codes#rate_limited",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded (30 req/sec). Retry after 1 second.",
"code": "rate_limited"
}