diff --git a/app/utils/sendFile.js b/app/utils/sendFile.js index a9cb0d9..e2815b1 100644 --- a/app/utils/sendFile.js +++ b/app/utils/sendFile.js @@ -96,6 +96,11 @@ async function sendFile(ctx, app, opts) { // 🔴 X-Accel-Redirect:把文件交给 nginx 内部 location 直接从磁盘流式吐出, // Node 只发响应头、不再持有文件流 → 极端并发下几乎零占用 worker。 const internalPath = cfg.internalPrefix.replace(/\/+$/, '/') + relativePath.replace(/^\/+/, ''); + // 🔴 X-Accel-Redirect 是 HTTP 头值,只允许 ASCII:中文/全角文件名必须**逐段 URL 编码** + // (保留 `/` 分隔符),否则 Node 的 res.setHeader 抛 ERR_INVALID_CHAR ⇒ 500。 + // 2026-10-12 生产实测:修复 :36 校验放行中文名后,本行成为新的失败点(400 → 500)。 + // nginx 按**解码后**的 URI 匹配 internal location,再由 alias 落到磁盘真实中文文件名。 + const internalUri = '/' + internalPath.replace(/^\/+/, '').split('/').map(encodeURIComponent).join('/'); ctx.set('Content-Type', contentType); // 🔴 下载完整性哨兵:把「服务端认定的真实字节数」透出给前端。 // X-Accel 分支下真正吐字节的是 nginx,Node 无从得知 nginx 是否吐对了东西 @@ -105,7 +110,8 @@ async function sendFile(ctx, app, opts) { // 中文名按 RFC 5987 编码(filename*=UTF-8''),浏览器自动解码; // 切勿再包 encodeURIComponent 造成双重编码。 ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); - ctx.set('X-Accel-Redirect', internalPath); + // ⚠️ 必须用编码后的 internalUri(纯 ASCII),不可直接用含中文的 internalPath。 + ctx.set('X-Accel-Redirect', internalUri); ctx.set('X-Accel-Buffering', 'no'); // 大文件边收边发,避免 nginx 缓冲撑爆内存 ctx.status = 200; ctx.body = null;