用 request_id 和 X-Request-Id 追踪 UnifyPort API 错误
排查 UnifyPort API 错误时,保存 JSON 响应中的 request_id 或响应头中的 X-Request-Id。你也可以在请求头中发送自己的 X-Request-Id,JSON 响应会以 client_request_id 回传该值,方便关联应用日志。这些标识用于追踪请求,不是消息 ID、webhook 事件 ID,也不代表重复请求就能避免重复操作。
要点
- 区分业务操作、每次 HTTP 尝试和服务端请求标识。
- 即使没有 JSON 正文,也要读取响应头;成功的
204删除响应就是一例。 - 超时可能没有服务端请求 ID,操作结果也可能仍然未知。
- 记录结构化错误码,不记录凭据或完整消息内容。
应该保存哪个请求 ID?
API 入门文档定义了追踪契约。同一个头名称在两个方向上含义不同:
| 值 | 来源 | 用途 |
|---|---|---|
自己的 X-Request-Id | 客户端请求头 | 定位本地 HTTP 尝试 |
client_request_id | JSON 回传的客户端值 | 将响应关联到该次尝试 |
request_id | 服务端 JSON 响应 | 向支持团队提供服务端追踪标识 |
响应 X-Request-Id | 服务端响应头 | 没有正文时仍可保留追踪信息 |
data.message_id | 发送成功时返回的消息标识 | 标识消息,而非 HTTP 请求 |
六月 API 更新说明介绍过这些字段;本文解决的是如何在所有响应分支中保留它们,而不只是知道字段存在。
建议为“一次经批准的回复”这样的业务动作保留本地操作标识,再为每次 HTTP 尝试生成独立标识并建立关联。这是应用设计建议,不是额外的 UnifyPort 请求字段。标识中不要包含客户姓名、手机号、消息或密钥。
记录一次请求,不自动重试
先使用只读的当前工作区接口。以下 Node.js 示例使用内置 fetch,从后端环境变量 UNIFYPORT_API_KEY 读取密钥,只输出白名单诊断字段,不输出请求头或响应正文。
超时时间只是示例客户端策略,不是服务限制。函数只发起一次应用层请求,并将原始 Response 交还调用方。
import { randomUUID } from 'node:crypto';
async function tracedWorkspaceRead(apiKey, operationId) {
const clientAttemptId = randomUUID();
const startedAt = new Date().toISOString();
const startedMs = Date.now();
let response;
try {
response = await fetch('https://api.unifyport.ai/v1/workspace', {
headers: {
'X-Api-Key': apiKey,
'X-Request-Id': clientAttemptId
},
signal: AbortSignal.timeout(10000)
});
} catch {
console.info({
operation_id: operationId,
client_attempt_id: clientAttemptId,
started_at: startedAt,
elapsed_ms: Date.now() - startedMs,
outcome: 'no_http_response'
});
throw new Error('No HTTP response; inspect the local attempt record');
}
const headerId = response.headers.get('X-Request-Id');
const body = response.status === 204
? null
: await response.clone().json().catch(() => null);
console.info({
operation_id: operationId,
client_attempt_id: clientAttemptId,
started_at: startedAt,
elapsed_ms: Date.now() - startedMs,
http_status: response.status,
server_request_id_header: headerId,
server_request_id_body: body?.request_id ?? null,
echoed_client_request_id: body?.client_request_id ?? null,
error_code: body?.error?.code ?? null,
numeric_code: body?.error?.numeric_code ?? null
});
return response;
}
const apiKey = process.env.UNIFYPORT_API_KEY;
if (!apiKey) throw new Error('Configure UNIFYPORT_API_KEY');
await tracedWorkspaceRead(apiKey, randomUUID());
operation_id、outcome 等日志名称是本地应用字段,不是 API 响应结构。通用处理逻辑包含 204 分支,便于复用于文档明确无正文的操作;工作区读取成功本身返回 200。测试空响应请用本地模拟,不要为验证日志而删除消息账号。
同时保留头部和正文中的标识,可以发现缺失或不一致的响应。中间代理返回的内容未必遵循 API 信封格式;应保存 HTTP 状态和本地尝试记录,不要伪造服务端 ID。生产环境还需限制解析与日志大小,设置访问权限和保留期限。
将记录整理成有效的故障报告
错误参考定义了 error.code、error.numeric_code 和 error.message。程序分支应依据 code 或 numeric_code,而不是面向人的错误说明。数值错误码可能细化上游原因,但不改变原有错误码或 HTTP 状态。
| 观察结果 | 保存的证据 | 下一步 |
|---|---|---|
| JSON 成功或错误 | 状态、服务端 ID、客户端回传值、错误码 | 按具体接口理解结果 |
204 空正文 | 状态和响应 X-Request-Id | 不要将无 JSON 当成接口失败 |
| 正文无法解析或格式异常 | 状态、可用的头部 ID、本地尝试 ID | 排查响应在哪一层发生变化 |
| 没有 HTTP 响应 | 本地尝试 ID、开始时间、业务操作 | 核实之前保留结果未知状态 |
提交支持请求时,提供环境、UTC 时间、HTTP 方法和路由模板、收到的服务端请求 ID、本地尝试 ID、状态与错误码,以及预期和实际行为。账号或会话标识只通过适当的受限渠道分享。不要附上 API 密钥、会话凭据、签名密钥、回复令牌或带访问签名的媒体 URL。
X Chat 故障排查指南展示了这些记录如何帮助区分账号故障与单个会话的问题。请求 ID 用于定位证据,并不直接证明故障原因。
追踪标识不等于重试许可
对于 POST /v1/messages,重复使用客户端请求 ID 不是文档承诺的幂等机制。发送超时时,即使客户端没有收到响应,操作也可能已经生效。先记录不确定性,再决定是否允许再次发送。
这与 LINE 明确的重试键契约不同:LINE 在重复接受响应中提供 x-line-accepted-request-id。不能因为 UnifyPort 有名称相近的追踪头,就推断具备同样的行为。具体流程见 LINE 重试键指南。
webhook 关联也是另一层。投递文档定义了 X-Device-Event-Id 和 X-Device-Delivery-Id;后者可能回退为事件 ID,不保证每次 HTTP 尝试都唯一。不要仅因 REST 请求和事件都有 ID,就强行关联。需要接收端尝试唯一标识时,请本地生成。事件去重应遵循各事件规则,包括 HistorySync 的特殊边界,不能由日志设计替代。
常见问题
为什么成功删除后看不到 request_id?
成功的 204 没有 JSON 正文,应读取响应 X-Request-Id 头。
能通过 request_id 查询发送状态吗?
追踪契约未定义请求状态查询接口。保存真实接口结果,通过受支持的证据核实不确定操作,不要根据 ID 拼接一个新 URL。
client_request_id 是幂等键吗?
文档没有这样的保证。它回传客户端请求头用于关联,不负责防止重复发送。
下一步与来源
先在一个现有 API 客户端中加入诊断摘要,用本地模拟测试 JSON 错误、空响应、异常正文和网络失败。错误分支以错误参考为准。
核对日期:2026-10-01。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。