php runtime 中 headers already sent 问题解决方案

问题

  • 问题1:
    最近有不少用户反馈使用 php runtime的时候遇见如下报错

Cannot modify header information - headers already sent by (output started at ...

  • 问题2:
    如果更改php 的session 目录?

本文旨在梳理此类问题的原因,触发条件以及相应的解法, 在介绍原因之前,先过一遍php runtime http trigger 使用的两种方式。

php runtime 使用FAQ 主目录

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

具体详情参考

php runtime 中 headers already sent 问题解决方案

  • 函数计算的执行环境相当于传统 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 目录 并且自动开启

上一篇:函数计算 python runtime 常用的第三方包下载


下一篇:冬季实战营第二期:Linux操作系统实战入门