
给 COS 编写上传适配器时,一个容易忽略的边界是:Complete Multipart Upload 的 HTTP 状态与业务结果不是同一层证据。官方文档说明,合并可能需要数分钟,开始合并时会先返回 200,通过 Transfer-Encoding: chunked 继续传输响应,最终结果需要读取完整响应体才能判断。
因此,不能在拿到响应头时就把数据库中的文件标记为“可下载”。正确顺序是:完整读取响应、解析 XML、识别成功或错误结果,再提交业务状态。本文讨论直接调用 REST API 的适配层;使用官方 SDK 时,应先确认 SDK 已经处理的语义,不要重复实现一套协议解析。
CompleteMultipartUploadResult,且必要字段符合预期,才进入成功分支。如果读取中途连接断开,结果应是“未知”,不是“明确失败”。服务端可能已经完成合并,只是客户端没有收到最终响应。把未知直接当失败并重新启动整套上传,既可能浪费流量,也可能覆盖同名对象。
业务状态可以采用 UPLOADING → COMPLETING → AVAILABLE,其中网络中断进入 UNKNOWN,由后续核查决定去向。这是应用设计建议,不是 COS 返回的枚举值。
下面是浏览器或现代 JavaScript 运行环境可运行的解析函数。输入 xml 必须来自已完整读取的响应体;DOMParser 在普通 Node.js 中不是内置对象,服务端应替换为经过维护的 XML 解析库,并禁用外部实体等不需要的能力。
function classifyComplete(status, xml) {
const doc = new DOMParser().parseFromString(
xml, 'application/xml'
);
if (doc.getElementsByTagName('parsererror').length) {
return { state: 'UNKNOWN', reason: 'invalid XML' };
}
const root = doc.documentElement;
const field = name =>
Array.from(root.children)
.find(e => e.localName === name)?.textContent ?? '';
if (root.localName === 'Error') {
return {
state: 'ERROR',
code: field('Code'),
requestId: field('RequestId')
};
}
if (status !== 200 ||
root.localName !== 'CompleteMultipartUploadResult') {
return { state: 'UNKNOWN', reason: 'unexpected response' };
}
if (!field('ETag') || !field('Bucket') || !field('Key')) {
return { state: 'UNKNOWN', reason: 'missing result fields' };
}
return {
state: 'SUCCESS',
bucket: field('Bucket'),
key: field('Key'),
etag: field('ETag')
};
}这里用 XML 根节点而不是字符串包含判断,避免错误描述中出现成功节点名称就误判。返回 SUCCESS 后,还应在调用方核对桶与对象键是否属于本次上传;对象键含特殊字符时,按接口约定处理编码,不要盲目做多次 URL 解码。
ERROR 只表示收到了明确错误响应,不表示所有错误都能自动重试;是否重试要结合错误码。空正文、截断 XML、代理返回 HTML 等情况进入 UNKNOWN,不能伪装成上传成功。
把上面的函数复制到浏览器开发者工具,再运行以下代码。这些是构造的协议样例,不是线上上传结果;目的是检查适配层是否仅凭 200 判成功。
const success = '<CompleteMultipartUploadResult>' +
'<Bucket>demo-1250000000</Bucket><Key>report.csv</Key>' +
'<ETag>example-etag</ETag></CompleteMultipartUploadResult>';
const error = '<Error><Code>InternalError</Code>' +
'<RequestId>example-request</RequestId></Error>';
const cases = [
[200, success, 'SUCCESS'],
[200, error, 'ERROR'],
[200, '<CompleteMultipartUploadResult>', 'UNKNOWN'],
[502, '<html>gateway failure</html>', 'UNKNOWN'],
[200, '<CompleteMultipartUploadResult/>', 'UNKNOWN']
];
for (const [status, body, expected] of cases) {
const actual = classifyComplete(status, body).state;
if (actual !== expected) {
throw new Error(`${expected} != ${actual}`);
}
}
console.log('5 cases passed');集成验收还应覆盖“响应读取到一半断开”。这发生在解析函数之前,调用方必须捕获读取异常并保留未知状态,不能因为函数没返回错误就默认成功。
提交合并前,持久化本次桶、对象键、UploadId、分块编号及对应 ETag,并关联业务上传记录。提交清单使用各分块上传返回的 ETag,按 PartNumber 升序组织;不要临时猜测或重算这些标识。
发生超时后,先核查目标对象。只看到对象存在仍不足以证明本次合并成功:同名键可能有历史对象或并发写入。使用每次上传独立的对象键,可以减少这种歧义;同时结合预期长度、校验信息与业务记录核对。不要把分块对象的 ETag 无条件当作整个文件的 MD5。
日志保留阶段、HTTP 状态、错误码和请求标识即可,避免记录签名头、临时密钥与完整授权 URL。客户端超时阈值应覆盖响应体读取,而不仅是建立连接;盲目调大超时不能替代未知结果核查。
最终原则很简单:响应头是传输证据,完整 XML 是接口结果证据,对象核查是异常恢复证据。三者各司其职,才能避免出现“页面提示上传完成,下载时却找不到文件”的状态错位。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。