fix(download): X-Accel-Redirect 头值须 URL 编码(修复中文名下载 500)
承接 c94d8fe:上一提交把中文名从 400「非法文件路径」放行后,暴露出 同一族 bug 的第二处漏网 —— sendFile.js:98 把含中文的 relativePath 直接拼进 X-Accel-Redirect 头值,Node res.setHeader 抛 ERR_INVALID_CHAR ⇒ 500(生产实测,common-error.log 为铁证)。 该分支仅在 NODE_ENV=production(X-Accel 开启)下命中,本地 dev 走 Node 流分支 ⇒ 永远测不出,典型「本地绿、生产红」陷阱。 修法:内部路径逐段 encodeURIComponent(保留 / 分隔符)得到纯 ASCII 的 internalUri;nginx 按解码后 URI 匹配 internal location,alias 落到 磁盘真实中文文件名。 验证:X-Accel 头编码判别力自证 PASS=7/FAIL=0(旧写法必抛 ERR_INVALID_CHAR;新写法头全 ASCII、逐段解码 == 原路径); 经 nginx 生产端到端 PASS=7/FAIL=0(中文名 200、bytes==磁盘 size、 下载内容 sha256 == 源文件 sha256、RFC5987 头正确); 直连 8023 对照 204/0 字节(反证字节确由 nginx 吐出)。
This commit is contained in:
@@ -96,6 +96,11 @@ async function sendFile(ctx, app, opts) {
|
|||||||
// 🔴 X-Accel-Redirect:把文件交给 nginx 内部 location 直接从磁盘流式吐出,
|
// 🔴 X-Accel-Redirect:把文件交给 nginx 内部 location 直接从磁盘流式吐出,
|
||||||
// Node 只发响应头、不再持有文件流 → 极端并发下几乎零占用 worker。
|
// Node 只发响应头、不再持有文件流 → 极端并发下几乎零占用 worker。
|
||||||
const internalPath = cfg.internalPrefix.replace(/\/+$/, '/') + relativePath.replace(/^\/+/, '');
|
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);
|
ctx.set('Content-Type', contentType);
|
||||||
// 🔴 下载完整性哨兵:把「服务端认定的真实字节数」透出给前端。
|
// 🔴 下载完整性哨兵:把「服务端认定的真实字节数」透出给前端。
|
||||||
// X-Accel 分支下真正吐字节的是 nginx,Node 无从得知 nginx 是否吐对了东西
|
// X-Accel 分支下真正吐字节的是 nginx,Node 无从得知 nginx 是否吐对了东西
|
||||||
@@ -105,7 +110,8 @@ async function sendFile(ctx, app, opts) {
|
|||||||
// 中文名按 RFC 5987 编码(filename*=UTF-8''<encodeURIComponent>),浏览器自动解码;
|
// 中文名按 RFC 5987 编码(filename*=UTF-8''<encodeURIComponent>),浏览器自动解码;
|
||||||
// 切勿再包 encodeURIComponent 造成双重编码。
|
// 切勿再包 encodeURIComponent 造成双重编码。
|
||||||
ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`);
|
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.set('X-Accel-Buffering', 'no'); // 大文件边收边发,避免 nginx 缓冲撑爆内存
|
||||||
ctx.status = 200;
|
ctx.status = 200;
|
||||||
ctx.body = null;
|
ctx.body = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user