如何追踪 payment gateway 请求的 503 timeout error?
结论
仅凭 503,无法判断支付是否失败,也无法确认客户端是否发生了超时。
要区分具体情况,需要同时记录 HTTP 状态码、请求耗时、网络异常、支付业务流水号和响应内容:
- 收到 HTTP
503 Service Unavailable:支付网关或其上游服务暂时不可用。 - 客户端等待时间超过设定值:发生本地请求超时,客户端可能没有收到任何 HTTP 响应。
- 连接被重置、DNS 失败或 TLS 失败:这些是网络错误,不属于 HTTP
503。 - 请求虽然超时,但支付已经被网关受理:支付结果暂时无法确定,不能直接判定为失败。
处理支付接口时需要特别注意:超时只表示没有及时拿到结果,不代表交易没有发生。
建议记录哪些信息
每次发起支付前,应先在系统中生成唯一的业务流水号,例如 merchantReference。日志至少需要包含:
- 业务流水号
- 请求开始和结束时间
- 请求耗时
- 请求地址和 HTTP 方法
- HTTP 状态码
- 响应头中的请求追踪 ID(如果有)
- 响应正文或错误码
- 网络异常类型
- 本地订单号
- 重试次数
日志中不要保存完整银行卡号、CVV、密码、访问令牌等敏感信息。支付日志应当脱敏,并设置严格的访问权限。
如何判断是哪一种超时
收到了 503 响应
如果程序拿到了 HTTP 响应,状态码也是 503,说明请求已经到达某个 HTTP 服务,但该服务暂时无法处理。
可以进一步检查:
- 是否有
Retry-After响应头; - 响应正文是否包含网关错误码;
- 响应头中是否有 request ID 或 correlation ID;
- 同一时间段内是否有大量请求出现相同错误。
503 通常表示服务暂时不可用。按照 HTTP 标准,更直接表示网关等待上游超时的是 504 Gateway Timeout。不过,具体支付服务也可能用 503 表示内部依赖超时,因此要以 Ezidebit 实际返回的响应内容和官方说明为准。
没有收到任何 HTTP 响应
如果客户端抛出 TimeoutException、AbortError、socket timeout 等异常,同时没有 HTTP 状态码,超时可能发生在客户端、网络链路或连接层。
此时,网关可能已经收到并处理了请求,只是响应没有成功返回。交易状态应标记为“待确认”,不能马上重新扣款。
推荐的处理流程
- 创建支付请求前,生成唯一的业务流水号。
- 将订单状态改为“支付处理中”。
- 设置合理的连接超时和响应超时。
- 完整记录响应状态、请求耗时和异常信息。
- 正常收到成功结果后,更新支付状态。
- 遇到
503、504或网络超时时,将状态改为“待确认”。 - 使用业务流水号或支付网关返回的交易标识查询交易状态。
- 如果 Ezidebit 支持异步通知,结合 webhook 更新最终状态。
- 如果查询后仍无法确认,进入延迟重查或人工对账流程。
- 只有确认原请求没有创建交易后,才能重新发起支付。
查询接口、业务流水号字段和 webhook 能力取决于所使用的 Ezidebit API 产品及版本,需要对照相应的接口文档确认。
Node.js 示例
下面的示例用于区分本地超时、HTTP 503 和其他 HTTP 错误。接口地址和请求字段只是结构示例,需要替换成实际使用的 Ezidebit 接口。
import crypto from "node:crypto";
async function submitPayment(paymentData) {
const merchantReference = crypto.randomUUID();
const controller = new AbortController();
const startedAt = Date.now();
const timeout = setTimeout(() => {
controller.abort();
}, 15000);
try {
const response = await fetch(process.env.PAYMENT_GATEWAY_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.PAYMENT_API_TOKEN}`
},
body: JSON.stringify({
merchantReference,
...paymentData
}),
signal: controller.signal
});
const elapsedMs = Date.now() - startedAt;
const responseBody = await response.text();
console.info({
event: "payment_gateway_response",
merchantReference,
status: response.status,
elapsedMs,
retryAfter: response.headers.get("retry-after"),
gatewayRequestId:
response.headers.get("x-request-id") ??
response.headers.get("x-correlation-id"),
responseBody: redactSensitiveData(responseBody)
});
if (response.ok) {
return {
state: "completed",
merchantReference,
responseBody
};
}
if (response.status === 503 || response.status === 504) {
return {
state: "pending_confirmation",
merchantReference,
reason: `gateway_http_${response.status}`
};
}
return {
state: "failed",
merchantReference,
reason: `gateway_http_${response.status}`,
responseBody
};
} catch (error) {
const elapsedMs = Date.now() - startedAt;
if (error.name === "AbortError") {
console.warn({
event: "payment_gateway_timeout",
merchantReference,
elapsedMs
});
return {
state: "pending_confirmation",
merchantReference,
reason: "client_timeout"
};
}
console.error({
event: "payment_gateway_network_error",
merchantReference,
elapsedMs,
errorName: error.name,
errorMessage: error.message
});
return {
state: "pending_confirmation",
merchantReference,
reason: "network_error"
};
} finally {
clearTimeout(timeout);
}
}
function redactSensitiveData(value) {
return value
.replace(/("cardNumber"\s*:\s*")[^"]+(")/gi, "$1***$2")
.replace(/("cvv"\s*:\s*")[^"]+(")/gi, "$1***$2")
.replace(/("token"\s*:\s*")[^"]+(")/gi, "$1***$2");
}
调用方应根据返回状态采取不同的处理方式:
const result = await submitPayment(paymentData);
switch (result.state) {
case "completed":
await markOrderAsPaid(result.merchantReference);
break;
case "pending_confirmation":
await markOrderAsPending(result.merchantReference);
await schedulePaymentStatusCheck(result.merchantReference);
break;
case "failed":
await markOrderAsFailed(result.merchantReference, result.reason);
break;
}
关于自动重试
支付请求不能像普通查询请求那样无条件重试。第一次请求可能已经完成扣款,直接重试可能造成重复交易。
只有满足以下条件时,自动重试才相对安全:
- 接口明确支持 idempotency key;
- 每次重试都使用相同的幂等键或业务流水号;
- 网关保证相同幂等键不会重复创建交易;
- 或者已经通过状态查询确认原请求没有成功。
对于明确允许重试的 503,可以采用指数退避,例如依次等待 1s、2s、4s,并加入少量随机抖动。如果响应中包含 Retry-After,应优先按照该值等待。支付创建请求能否重试,仍要以对应接口的幂等规则为准。
排查时的注意事项
- 确认
503是由 Ezidebit 返回的,还是由网站前面的 Nginx、负载均衡器、CDN 或云平台返回的。 - 对比应用日志、反向代理日志和支付请求时间,查清错误发生在哪一层。
- 保存网关提供的 request ID。联系支持人员时,一并提供请求时间、时区和业务流水号。
- 不要把“待确认”直接显示为“支付失败”。可以提示用户“支付结果确认中,请勿重复支付”。
- 建立定时对账任务,用于处理 webhook 丢失、响应超时和长时间未确认的订单。
503的响应正文可能是 HTML,也可能是 JSON。解析前应检查Content-Type,避免因错误页格式不符而再次触发解析异常。
备注:内容仅供参考。
