Ali-OSS 默认 30s 超时太激进,三层回退只挡 404 不挡抖动
某个文件预览接口突然开始大面积 500。报错信息直接印在响应体里:
Response timeout for 30000ms, please increase the timeout,
see more details at https://github.com/ali-sdk/ali-oss#responsetimeouterror
奇怪的是,用 curl 直接打 OSS 端点,215ms 就回来了,端点本身没问题。但 Node.js 进程内的 client.get() 挂了整整 30 秒才抛错——网络没断,只是慢。
现象
- 触发接口:任何调用了
oss.get()/oss.put()的请求 - 错误类型:
err.name === 'ResponseTimeoutError',状态码 500 - 频率:不稳定,有时正常,有时连续失败
- 本地 curl 探测 OSS 可达,DNS 解析返回的却是
198.18.x.x(RFC 2544 测试保留段)——透明代理或 DNS 劫持把流量拖慢了
根因
坑一:ali-oss SDK 6.x/7.x 默认 timeout: 30000(毫秒)
这个 timeout 是"完整请求-响应窗口",不是 TCP 连接超时。公网出口、跨地域链路、VPN 路由、DNS 污染——任何一个环节偶发延迟,30s 就不够用了。到点直接抛 ResponseTimeoutError,不重试,直接 500。
坑二:三层存储回退只认 NoSuchKey,不认超时
服务有一套优雅降级逻辑:OSS 读不到 → 查 MySQL → 读本地磁盘。但这套回退的过滤条件是:
if (!isOssNotFound(err)) throw err // 只处理 404/NoSuchKey而超时错误的 err.name 是 'ResponseTimeoutError',err.code 是 undefined(或反过来,取决于 SDK 版本),和 NoSuchKey 没有任何交集。结果:本地磁盘上躺着一份完整的不可变模板文件,愣是用不上,直接抛给用户一个 500。
NoSuchKey 在 err.code,ResponseTimeoutError 在 err.name(或有时也在 err.code,版本不同)。只检查一个字段的 isOssNotFound 判断,对超时错误视而不见,回退逻辑形同虚设。
正解
两处修改配合使用:
修复一:把 timeout 做成环境变量可覆盖,默认抬到 60s
const ossClient = new OSS({
region: process.env.OSS_REGION,
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: process.env.OSS_BUCKET,
secure: true,
// 公网/跨地域链路偶发 >30s,抬到 60s;OSS_TIMEOUT_MS=120000 可按需覆盖
timeout: Number(process.env.OSS_TIMEOUT_MS) || 60000,
})修复二:读路径的回退扩展到瞬时网络错误(仅限不可变资产)
function isOssTransient(err) {
if (!err) return false
const name = String(err.name || '')
const code = String(err.code || '')
if (name === 'ResponseTimeoutError' || name === 'RequestTimeoutError') return true
if (code === 'ResponseTimeoutError' || code === 'RequestTimeoutError') return true
if (['ECONNRESET', 'ETIMEDOUT', 'EAI_AGAIN', 'ENETUNREACH'].includes(code)) return true
return false
}
export async function readStoredFile(filePath) {
if (ossConfigured()) {
try {
return await readOssFile(filePath)
} catch (err) {
// NoSuchKey → 老数据迁移回退;ResponseTimeoutError → 网络抖动回退
if (!isOssNotFound(err) && !isOssTransient(err)) throw err
if (isOssTransient(err)) {
console.warn('[oss] transient error, falling back', { name: err?.name, code: err?.code })
}
}
}
if (mysqlConfigured()) {
const data = await readMysqlFile(filePath)
if (data) return data
}
return fs.readFile(filePath)
}如果 OSS 写超时后降级写本地磁盘,多机部署下 B 服务器找不到 A 服务器写的文件,用户拿到 broken link。写路径失败就应该大声报错,让用户重试,而不是悄悄写到本地再藏着。
isOssTransient 同时检查 err.name 和 err.code 两个字段,加上 Node.js 底层 socket 错误码(ECONNRESET / ETIMEDOUT),覆盖不同 SDK 版本的差异。读路径回退只对不可变资产开放——模板文件、归档上传件、生成文档——有本地副本则降级服务,无本地副本维持原有 500 行为,不引入数据不一致风险。
验证步骤
- 从 Node.js REPL 直接
await client.get('your/key')并计时——若超 30s 抛ResponseTimeoutError,诊断确认 - 设置
OSS_TIMEOUT_MS=60000(或依赖新默认值),重启服务 - 复现原来 500 的接口,观察是否成功或降级到本地(不再是 500)
- 查服务日志,确认出现
[oss] transient error, falling back行——证明新分支在生效
一句话外卖
ali-oss 的
timeout默认 30s 对公网链路太紧;三层回退若只挡NoSuchKey就是只挡文件不存在、不挡网络抖动——超时错误藏在err.name里,不在err.code里,必须同时检查。