问题
- 问题1:
最近有不少用户反馈使用 php runtime的时候遇见如下报错
Cannot modify header information - headers already sent by (output started at ...
- 问题2:
如果更改php 的session 目录?
本文旨在梳理此类问题的原因,触发条件以及相应的解法, 在介绍原因之前,先过一遍php runtime http trigger 使用的两种方式。
php runtime http trigger的使用姿势
使用 php runtime http trigger 的时候中一般有两种使用方式:
不使用 fcPhpCgiProxy
官方文档使用方法,当不需要进行部署较大的php工程的使用方法, 比如:
<?php
use RingCentral\Psr7\Response;
function handler($request, $context): Response{
return new Response(
200,
array(
"custom_header1" => "v1",
"custom_header2" => ["v2", "v3"],
"Set-Cookie" => urlencode("test php") . '=' . urlencode('test;more')
),
"hello world"
);
}
注意:强烈建议您所有返回的headers都放在构造Response对象的参数里面,如上面示例,不要单独使用能改变header的方法,比如 header 、 setcookie 等
使用 fcPhpCgiProxy
此时的函数入口相当传统Apache/Nginx 的 conf 中 location, 将对应的php 文件送给 php-fpm 去解析,然后将解析后的结果返回给用户
详解fcPhpCgiProxy
- 函数计算的执行环境相当于传统 web 服务的 Apache/Nginx
- 用户函数相当于实现 Apache/Nginx 的 conf 中 location
- 用户将 Web 网站部署在 NAS,然后挂载 NAS 到函数的执行环境,
函数计算为用户提供了一个 $GLOBALS['fcPhpCgiProxy']
对象用来和 php-fpm 进行交互,对
PHP 工程中的 php 文件进行解析,该对象提供了两个重要的接口:
requestPhpCgi($request, $docRoot, $phpFile = "index.php", $fastCgiParams = [], $options = [])
requestPhpCgi
$request
: 跟php http invoke
入口的参数一致$docRoot
: Web 工程的根目录$phpFile
: 用于拼接 cgi 参数中的 SCRIPT_FILENAME 的默认参数$fastCgiParams
: 函数计算内部尽量根据$request
给您构造default cgi params
, 但是如果您不是想要的,可以使用$fastCgiParams
覆盖一些参数 (reference: cgi)$options
: array类型,可选参数,debug_show_cgi_params
设为true
,会打印每次请求 php 解析时候的 cgi 参数, 默认为 false ;readWriteTimeout
设置解析的时间, 默认为 5 秒
原因
错误出现的原因简单描述就是当php 收到第一个 output (print, echo, )时,它会flush所有收集到的headers。然后它可以发送它想要的所有输出。但是这个时候再发送HTTP header是不允许的。详情可以参考:how-to-fix-headers-already-sent-error-in-php
通常来说就是用户第一种方式(不使用 fcPhpCgiProxy) 使用http trigger的时候,使用如下方法可能会出现以下问题:
解法
不使用 fcPhpCgiProxy
<?php use RingCentral\Psr7\Response; function handler($request, $context): Response{ return new Response( 200, array( "custom_header1" => "v1", "custom_header2" => ["v2", "v3"], "Set-Cookie" => urlencode("test php") . '=' . urlencode('test;more') ), "hello world" ); }
不单独使用能改变header的方法,比如 header 、 setcookie 等,将您所有返回的headers都放在构造Response对象的参数里面
使用fcPhpCgiProxy
至少两个文件, 一个入口函数文件,一个待解析的文件, for example:<?php // 入口函数文件 function http_fc_func_cgi($request, $context): Response{ $requestURI = $request->getAttribute("requestURI"); // parse $requestURI $proxy = $GLOBALS['fcPhpCgiProxy']; return $proxy->requestPhpCgi($request, __DIR__ . '/www', "a.php", ['SERVER_NAME' => 'abc.com']); }
<?php // 待解析的文件 $body = @file_get_contents('php://input'); http_response_code(500); header('customheader1: v1'); header('customheader1: v3', false); header('customheader2: v2'); echo "<HTML>" . "\n"; echo "<HEAD><TITLE>Simple Virtual HTML Document</TITLE></HEAD>" . "\n"; echo "<BODY>" . $body . "\n"; echo "REQUESTHEADERS: " . "\n"; foreach ($_SERVER as $key => $value) { if (strpos($key, 'HTTP_') === 0) { echo $key . " : " . $value . PHP_EOL; } } echo "QUERYSTRING: " . $_SERVER['QUERY_STRING'] . PHP_EOL; echo "<H1>" . "Virtual HTML" . "</H1>" . "<HR>" . "\n"; echo "Hey look. I just created a virtual (yep. virtual) HTML document!" . "\n"; echo "</BODY></HTML>" . "\n"; ?>
建议: 如果只是使用http trigger 实现类似一个api的功能,尽量不要使用
fcPhpCgiProxy
, 即不要单独使用改变header的方法; 如果是类似一个web的工程的开发或者迁移,尽量使用fcPhpCgiProxy
session 目录更改的姿势
由于函数计算serverless的特性,我们会把公共的东西放在nas目录,比如web 工程, 临时tmp缓存; 在php runtime使用场景中, session目录肯定是想放在nas 公共目录中的.
参考函数计算 php runtime - 如何加载卸载内置扩展
只要是在php ini 可以设置的,我们都可以通过上述的方法达到目标,并且我们鼓励使用这种方法来裁剪不需要的extension, 比如针对session 设置,我们可以使用如下自定义的ini:
extension=session.so
extension=ftp.so
extension=shmop.so
extension=bcmath.so
extension=gettext.so
extension=pcntl.so
extension=simplexml.so
extension=xmlreader.so
extension=bz2.so
extension=gmp.so
extension=pdo.so
extension=soap.so
extension=xmlrpc.so
extension=calendar.so
extension=iconv.so
extension=pdo_mysql.so
extension=sockets.so
extension=xmlwriter.so
extension=ctype.so
extension=imagick.so
extension=phar.so
extension=sysvmsg.so
extension=dom.so
extension=json.so
extension=posix.so
extension=sysvsem.so
extension=exif.so
extension=mbstring.so
extension=protobuf.so
extension=sysvshm.so
extension=fileinfo.so
extension=mysqli.so
extension=redis.so
extension=zip.so
extension=memcached.so
extension=tokenizer.so
session.save_path=/mnt/www
session.auto_start=1
最后两句用于session的设置,设置到nas的 /mnt/www
目录 并且自动开启