nginx上,http状态200响应,PHP空白返回的问题

最近的大半年中,编程语言从PHP换到了Golang后,就很少接触PHP,当然,更多的还是恋恋不舍。尽管如此,每当有人在群里聊起PHP的话题时,我总是想插几句,怀念怀念,同时也温故温故知识点,可不能把她给忘了。

昨天朋友tywei问我一个关于PHP奇怪问题,查到原因解决后,没有详细的解释。夜里睡觉时,老是回想这事,早上醒来,决定还是认真记录一下这些问题。也让自己回归正常状态,多写点博客,总结自己,记录自己。

问题描述
PHP+nginx的环境,任何PHP处理的结果,都是空白页面。OS是ubuntu 14.10 ,nginx 1.6.2 ,PHP5.5.12, 问任何PHP的页面,返回的HTTP状态200,但页面内容是空的,什么都没有,不管PHP页面里写的是什么,正常响应。初步怀疑这是拓展的问题,处理请求后,输出内容有冲突,输出为空之类。想查看拓展列表,看看加载了哪些拓展,但任何PHP代码都返回空,万不得已,在CLI模式下运行,确定ini是同一个。粗略的看了加载的模块,列表如下:

Configuration File (php.ini) Path => /etc/php5/cli
Loaded Configuration File => /etc/php5/cli/php.ini
Scan this dir for additional .ini files => /etc/php5/cli/conf.d
Additional .ini files parsed => /etc/php5/cli/conf.d/05-opcache.ini,
/etc/php5/cli/conf.d/10-mysqlnd.ini,
/etc/php5/cli/conf.d/10-pdo.ini,
/etc/php5/cli/conf.d/20-curl.ini,
/etc/php5/cli/conf.d/20-gd.ini,
/etc/php5/cli/conf.d/20-imagick.ini,
/etc/php5/cli/conf.d/20-json.ini,
/etc/php5/cli/conf.d/20-memcache.ini,
/etc/php5/cli/conf.d/20-memcached.ini,
/etc/php5/cli/conf.d/20-mysql.ini,
/etc/php5/cli/conf.d/20-mysqli.ini,
/etc/php5/cli/conf.d/20-pdo_mysql.ini,
/etc/php5/cli/conf.d/20-readline.ini,
/etc/php5/cli/conf.d/20-redis.ini,
/etc/php5/cli/conf.d/20-xdebug.ini

大约如上的模块加载,怀疑xdebug跟opcache冲突,尝试关闭后,仍未解决。
strace看系统调用信息

10:02:03.432445 accept(0, {sa_family=AF_INET, sin_port=htons(49617), sin_addr=inet_addr("127.0.0.1")}, [16]) = 4
10:02:06.125142 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917
10:02:06.125177 poll([{fd=4, events=POLLIN}], 1, 5000) = 1 ([{fd=4, revents=POLLIN}])
10:02:06.125332 read(4, "\1\1\0\1\0\10\0\0", 8) = 8
10:02:06.125363 read(4, "\0\1\0\0\0\0\0\0", 8) = 8
10:02:06.125381 read(4, "\1\4\0\1\3)\7\0", 8) = 8
10:02:06.125394 read(4, "\f\0QUERY_STRING\16\3REQUEST_METHODGET\f\0CONTENT_TYPE\16\0CONTENT_LENGTH\v\nSCRIPT_NAME/index.php\v\1REQUEST_URI/\f\nDOCUMENT_URI/index.php\r\22DOCUMENT_ROOT/data/web/test.com\17\10SERVER_PROTOCOLHTTP/1.1\21\7GATEWAY_INTERFACECGI/1.1\17\vSERVER_SOFTWAREnginx/1.6.2\v\10REMOTE_ADDR10.0.2.2\v\5REMOTE_PORT50057\v\tSERVER_ADDR10.0.2.15\v\2SERVER_PORT80\v\10SERVER_NAMEtest.com\17\3REDIRECT_STATUS200\t\10HTTP_HOSTtest.com\17\nHTTP_CONNECTIONkeep-alive\22\tHTTP_CACHE_CONTROLmax-age=0\vJHTTP_ACCEPTtext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\17yHTTP_USER_AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36\24\23HTTP_ACCEPT_ENCODINGgzip, deflate, sdch\24\27HTTP_ACCEPT_LANGUAGEzh-CN,zh;q=0.8,en;q=0.6\v\6HTTP_RA_VER2.10.1\v&HTTP_RA_SIDB4A714D2-20150327-061728-7c932d-97f1f0\0\0\0\0\0\0\0", 816) = 816
10:02:06.125416 read(4, "\1\4\0\1\0\0\0\0", 8) = 8
10:02:06.125439 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={60, 0}}, NULL) = 0
10:02:06.125468 rt_sigaction(SIGPROF, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, {0x6ec3d0, [PROF], SA_RESTORER|SA_RESTART, 0x7f3c26740da0}, 8) = 0
10:02:06.125508 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0
10:02:06.125570 times({tms_utime=0, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 1718358917
10:02:06.125601 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
10:02:06.125637 fcntl(3, F_SETLK, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0
10:02:06.125668 write(4, "\1\6\0\1\0@\0\0X-Powered-By: PHP/5.5.12-2ubuntu4.4\r\nContent-type: text/html\r\n\r\n\1\3\0\1\0\10\0\0\0\0\0\0\0\0\0\0", 88) = 88
10:02:06.125697 shutdown(4, SHUT_WR)    = 0
10:02:06.125713 recvfrom(4, "\1\5\0\1\0\0\0\0", 8, 0, NULL, NULL) = 8
10:02:06.125728 recvfrom(4, "", 8, 0, NULL, NULL) = 0
10:02:06.125797 close(4)                = 0

觉得好简短,好奇怪,访问的是SCRIPT_NAME index.php,怎么都没 lstat \open 这个PHP文件呢?直接返回了?起码要判断SCRIPT_FILENAME是否存在吧,要读取SCRIPT_FILENAME,解析里面的代码吧? 等下…..SCRIPT_FILENAME全部地址是啥?发来的CGI协议包中怎么没有SCRIPT_FILENAME?
仔细看下CGI包的内容

QUERY_STRING
REQUEST_METHODGET
CONTENT_TYPE
CONTENT_LENGTH

SCRIPT_NAME /index.php
REQUEST_URI /
DOCUMENT_URI /index.php
DOCUMENT_ROOT /data/web/test.com
SERVER_PROTOCOL HTTP/1.1
GATEWAY_INTERFACE CGI/1.1
SERVER_SOFTWARE nginx/1.6.2
REMOTE_ADDR 10.0.2.2
REMOTE_PORT 50057
SERVER_ADDR 10.0.2.15
SERVER_PORT 80
SERVER_NAME test.com
REDIRECT_STATUS 200

HTTP_HOST test.com
HTTP_CONNECTION keep-alive

缺少:

SCRIPT_FILENAME
//PATH_TRANSLATED //这个暂时无视

那么问题来了

  • PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
  • 发来的CGI协议包中,为啥没有SCRIPT_FILENAME? (SCRIPT_FILENAME是什么,干啥用的,这个就不要问了吧。)

问题1:PHP-FPM接收CGI请求时,如果没有SCRIPT_FILENAME怎么处理的?
fpm源码里(PHP5.5.x 为例)

//fpm_main.c
/* {{{ main
 */
int main(int argc, char *argv[])
{
	//...

	//fpm_main.c 1820行
	while (fcgi_accept_request(&request) >= 0) {
		request_body_fd = -1;
		SG(server_context) = (void *) &request;
		init_request_info(); 	//这里对应986行左右的的init_request_info 函数中代码
		char *primary_script = NULL;

		fpm_request_info();

		/* request startup only after we've done all we can to
		 *            get path_translated */
		if (php_request_startup() == FAILURE) {
			fcgi_finish_request(&request, 1);
			SG(server_context) = NULL;
			php_module_shutdown();
			return FPM_EXIT_SOFTWARE;
		}

		/* check if request_method has been sent.
		 * if not, it's certainly not an HTTP over fcgi request */
		if (!SG(request_info).request_method) {//这里判断request.method是否存在,在init_request_info方法里,最上面设置了默认的NULL
			goto fastcgi_request_done;
		}

		//...
		fastcgi_request_done:
			//结束当前request请求,给及响应

	}
}

同样是 fpm_main.c中init_request_info函数的代码如下:

//fpm_main.c 986行
static void init_request_info(void)
{
	char *env_script_filename = sapi_cgibin_getenv("SCRIPT_FILENAME", sizeof("SCRIPT_FILENAME") - 1);
	char *env_path_translated = sapi_cgibin_getenv("PATH_TRANSLATED", sizeof("PATH_TRANSLATED") - 1);
	char *script_path_translated = env_script_filename;
	char *ini;
	int apache_was_here = 0;

	/* some broken servers do not have script_filename or argv0
	 * an example, IIS configured in some ways.  then they do more
	 * broken stuff and set path_translated to the cgi script location */
	if (!script_path_translated && env_path_translated) {
		script_path_translated = env_path_translated;
	}

	/* initialize the defaults */
	SG(request_info).path_translated = NULL;
	SG(request_info).request_method = NULL;
	SG(request_info).proto_num = 1000;
	SG(request_info).query_string = NULL;
	SG(request_info).request_uri = NULL;
	SG(request_info).content_type = NULL;
	SG(request_info).content_length = 0;
	SG(sapi_headers).http_response_code = 200;	//	这里默认给了200的响应

	/* script_path_translated being set is a good indication that
	 * we are running in a cgi environment, since it is always
	 * null otherwise.  otherwise, the filename
	 * of the script will be retreived later via argc/argv */
	if (script_path_translated) {
		if (CGIG(fix_pathinfo)) {
			//对pathinfo做处理,剥离出SCRIPT_FILENAME,并重置SCRIPT_FILENAME
		} else {
		}

		if (is_valid_path(script_path_translated)) {
			//这里如果script_path_translated是合法路径,就给转化一下,赋值给SG(request_info).path_translated
			SG(request_info).path_translated = estrdup(script_path_translated);
		}

		SG(request_info).request_method = sapi_cgibin_getenv("REQUEST_METHOD", sizeof("REQUEST_METHOD") - 1);//这里从CGI包李获取method,赋值给request_method
		// ...
	}
}

从代码里看出,script_path_translated变量就是cgi协议包中SCRIPT_FILENAME的结果,其中1115行左右,判断如果script_path_translated为空,并且env_path_translated不为空,则用env_path_translated赋值到script_path_translated上。
之后,对request_info的几个属性给予了默认值,包括request_method为null,以及http_response_code默认200的http响应。
在后面的代码中if (script_path_translated) ,因为CGI包中没有SCRIPT_FILENAME,也没有PATH_TRANSLATED,即script_path_translated为空,故没有对request_method进行赋值,其默认值为NULL。
回到main函数中1839行附近if (!SG(request_info).request_method) ,则直接goto到了fastcgi_request_done,直接结束当前request请求,由于之前有设置过默认的http 响应状态为200 ,也就导致了每次返回http状态200成功响应的空白页面的问题。 同时也解释了strace系统调用中,没出现lstat、open 操作SCRIPT_FILENAME的记录了。

问题2,发来的CGI协议包中,为啥没有SCRIPT_FILENAME?
PHP-FPM接收到的CGI协议包都是来自前面nginx的,cgi协议包中没有这个,肯定是nginx没发来,查看nginx配置看到fastcgi_params中没有这项。加上后就可以了。

解决办法:
在nginx配置的 fastcgt_params中加上SCRIPT_FILENAME的配置(在ubuntu的apt-get形式安装nginx配置中,默认是有这条的),比如

  fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;

题外话:
在FPM的fpm_main.c文件的main函数中,1848行对SG(request_info).path_translated的判断,晚于1849行SG(request_info).request_method的判断。而在init_request_info函数中,对SG(request_info).path_translated的赋值却是比SG(request_info).request_method早的。 而且,明明找不到需要执行的脚本,却还返回200的http响应,很奇怪,也不方便排查。我觉得把对SG(request_info).request_method的判断放到SG(request_info).path_translated后面更合适一些。或者若找不到SCRIPT_FILENAME的话,http响应状态改为404,同时,写入LOG日志,便于排查。

后记:
从fpm代码里可以看到,其实作者是有考虑到没有SCRIPT_FILENAME的问题的,只是判断顺序搞错了。所以,我觉得这应该是个bug,就提了一下:BUG #69625:php-fpm return http 200 response on nginx without SCRIPT_FILENAME,不知道官方是否认为这是个BUG。

新的问题:

  • 为什么如果SCRIPT_FILENAME不存在时,用PATH_TRANSLATED来代替它?PATH_TRANSLATED是每个CGI前端都要发送的吗?

这个问题,后来认真看了下,感觉还挺复杂,跟CGI客户端有关,PHPFPM针对IIS\APACHE\NGINX的处理都不一样。以后再写吧。

参考资料:
RFC3875 – The Common Gateway Interface (CGI) Version 1.1

如果你在阅读PHP源码,或者阅读PHP SAPI、PHP拓展源码,可以关注一下PHP源码执行流程图 关于FPM的执行流程,可以看下下面这幅图:
fpm__main_8c_a0ddf1224851353fc92bfbff6f499fa97_cgraph

2018年07月09日更新:
Fix bugs #75120 and #69625 #3226里修复了我在2015年3月提的 bug report Fix bug #69625 php-fpm return http 200 response without SCRIPT_FILENAME #1270

关注微信公众号,手机阅读更方便: 程序员的阅微草堂

知识共享许可协议莿鸟栖草堂CFC4N 创作,采用 知识共享 署名-非商业性使用-相同方式共享(3.0未本地化版本)许可协议进行许可。基于http://www.cnxct.com上的作品创作。转载请注明转自:nginx上,http状态200响应,PHP空白返回的问题

8 thoughts on “nginx上,http状态200响应,PHP空白返回的问题

  1. 为什么我的nginx配置有fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    还会出现这种情况

  2. Hi there! Quick question that’s completely off topic.
    Do you know how to make your site mobile friendly? My weblog looks weird when viewing from my apple iphone.
    I’m trying to find a theme or plugin that might be able to correct this
    issue. If you have any suggestions, please share. With thanks!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据