【背景】
在我实际使用的环境中很少直接使用 nginx + php-fpm 方式搭建环境,大部分还都是使用apache,即便用到nginx,还只是用它监听80端口再代理apache的php做负载均衡器。
这次偶尔机会自己搭建了 nginx + php-fpm 环境,发现自己开发的MyQEE输出js、css以及image图片时会出现异常关闭的问题。

nginx配置了类似这样的rewrite

if (!-e $request_filename) {
    rewrite ^/.* /index.php last;
}

rewrite的意思是当请求的实际文件不存在时rewrite到index.php上

【那么问题来了】
由于有那个rewrite的存在,所以当请求类似 http://127.0.0.1/test.js 的URL,如果服务器上没有test.js,此时就会重定向导index.php,并不会直接返回404错误,而是由index.php来决定。

然后由这个index.php根据一些参数(例如uri=test.js)载入了另外一个js,并模拟页面输出,输出了一个200的头信息。
代码类似如下:

// index.php 文件,实际上的代码可能比这个复杂多,这里只是为了简单的说明问题
$file = 'my_other.js'; // 假设服务器上有另外一个my_other.js的文件
header('Content-Type: application/x-javascript');
header('Content-Length: '. filesize($file));
readfile($file);    // 把文件直接输出

这个看上去似乎没有任何问题,但是在nginx里却出现了奇怪问题。

浏览器再接受到部分内容输出后直接被关闭了。打开调试输出可以看到类似:
NET:ERR_CONTENT_LENGTH_MISMATCH
这样的错误。

如果在命令行里使用curl请求时,则会看到类似

curl: (18) transfer closed with 56097 bytes remaining to read

这样的错误。

【问题分析】
首先来看 nginx 的配置,类似如下:

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

这个实际上是nginx通过代理php-fpm来实现的输出,当一个css、js、图片等正常请求因为rewrite的原因被rewrite到了php上,nginx内部会认为是一个文档处理,然后对文档进行压缩获得了压缩后的内容长度,在输出达到这个长度后就错误的关闭了tcp连接,但是返回的header头信息的长度却是压缩前的长度,这样就导致了之前的错误。

【问题解决】
我尝试过把gzip的功能关闭但是实际上没有启到任何作用,最后我是通过这样的方法解决的,在gzip的gzip_types参数把对应的文件都加上这样就解决问题了,代码如下:

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
   
    # 以下解决用php输出js,css等文件导致出错的问题
    gzip on;
    gzip_min_length 1100;
    gzip_buffers 4 8k;
    gzip_types text/plain application/x-javascript text/css image;
}

2015-09-25更新
本人另外一个服务器实测也遇到这个问题,需要在加上才可以

proxy_buffering off

在nginx的默认配置里 gzip_types就只有 text/plain

【总结】
实际上的解决就是把对应输出的文件类型让nginx再压缩下即可,但是如果php输出的是图片或一些已经压缩过的二进制文件,那么这样做实际上是增加了服务器的负担。
所以如果有大量的图片、swf、zip等文件,建议还是通过sendfile的方式进行转发。
sendfile可参考: