ThinkPHP的response()->download()自动设置Content-Length但易出错:文件动态写入、NFS缓存、权限问题会导致filesize()失效,引发ERR_CONTENT_LENGTH_MISMATCH;须手动校验路径可读性并用realpath()后取size,大文件或代理环境需显式设且确保Nginx不覆盖该头。

ThinkPHP用response()->download()时Content-Length自动计算但不准
ThinkPHP 6+ 的 response()->download() 默认会调用 filesize() 设置 Content-Length,但这个值可能失效:文件被其他进程写入中、NFS挂载点 stat 缓存延迟、或文件权限导致 filesize() 返回 false(此时框架 fallback 为 0 字节)。结果就是浏览器卡在 pending,报 net::ERR_CONTENT_LENGTH_MISMATCH。
- 先手动校验:
if (!is_file($path) || !is_readable($path)) { abort(404); },再用$size = @filesize($path) ?: 0;避免警告 - 若文件可能动态增长(如日志),别依赖
filesize(),改用流式响应 + 显式Content-Length(见下一条) - 注意:
realpath($path)后再filesize(),防止符号链接绕过路径校验
大文件下载必须自己设Content-Length且不能出错
流式下载(比如用 Response::create() + fopen())不会自动算长度,你得手动传 Content-Length 值。一旦填错——比如用了 mb_strlen($content) 而不是 strlen($content),或没考虑 gzip 压缩——浏览器立刻判定不匹配,连接挂起或截断。
- 正确做法:用
filesize($filePath)获取原始字节数,不是 PHP 字符串长度 - 如果文件不可读(权限/不存在),
filesize()返回false,必须提前判断,否则Content-Length:变成空头,Nginx 或 Chrome 直接拒收 - 不要在流回调里做耗时操作(如数据库查询),否则响应头发出后长时间无数据,触发超时;
Content-Length是静态承诺,不能“边读边算”
Nginx代理下Content-Length被吃掉或覆盖
ThinkPHP 本地跑没问题,一上 Nginx 就报 ERR_CONTENT_LENGTH_MISMATCH,大概率是 Nginx 清除了或错误重写了 Content-Length 响应头。常见于加了 proxy_set_header Content-Length ""、或启用了 gzip on 却没关 gzip_http_version 1.0。
- 检查 Nginx 配置:删掉所有对
Content-Length的proxy_set_header覆盖,包括置空语句 - 确认没开
proxy_buffering on且proxy_buffer_size过小——缓冲区溢出会导致截断,Content-Length却还是原始值 - 若启用了
gzip,确保gzip_vary on和gzip_http_version 1.1,避免 Nginx 在压缩后忘记更新Content-Length
中文文件名+Content-Length组合最容易翻车
用 response()->download($path, '订单.xlsx') 看似简单,但 ThinkPHP 默认用 rawurlencode() 处理文件名,生成的 Content-Disposition 头可能让某些浏览器(尤其是旧版 Safari)忽略 Content-Length 校验,转而按 chunked 解析——而你的响应没发 Transfer-Encoding: chunked,协议不匹配直接崩。
立即学习“PHP免费学习笔记(深入)”;
- 强制走 RFC5987 编码:
['Content-Disposition' => 'attachment; filename*=UTF-8\'\''. rawurlencode('订单.xlsx')] - 同时显式设置
Content-Length和Content-Transfer-Encoding: binary,堵死浏览器猜测路径 - 别在文件名里拼接用户输入,
rawurlencode()不防路径遍历;先basename()再编码



















