PHP headers already sent 缘由分析

原文地址:PHP headers already sent 缘由分析php

先上结论,为了不 headers already sent 错误,你应该[^1]:html

  • 检查 PHP 代码,确认 <?php 前没有空格和空行
  • 避免在业务代码中使用 echo 和 print 系函数,只在框架组织 HTTP body 输出的时候使用,这些函数包括git

    • print, echo, printf, vprintf
    • trigger_error, ob_flush, ob_end_flush, var_dump, print_r
    • readfile, passthru, flush, imagepng, imagejpeg

缘由分析

最近上线代码以后遇到了一个问题,在某些状况下会抛出异常:Uncaught Exception: ErrorException: Severity: 2; Message: Cannot modify header information - headers already sent by...。并且这个异常并不是老是会出现,在不了解缘由的状况下想要在测试环境重现比较困难,如下是分析步骤。github

异常产生的缘由

它本质上是一个 E_WARNING,被 error_handler 截获而抛出异常:shell

<?php
function _error_handler($severity, $message, $filepath, $line) {
    // ...
    if (($severity & error_reporting()) == $severity)
    {
          // db rollback
        throw new ErrorException("Severity: $severity; Message: $message");
    }
}复制代码

在 index.php 中咱们设置 error_reporting 要报告 E_WARNING 错误,因此会走到这里并抛出异常。也就是说,咱们须要找到 E_WARNING 抛出的位置和缘由。服务器

E_WARNING 产生的缘由

<p>Severity: Warning</p>
<p>Message:  Cannot modify header information - headers already sent by (output started at .../application/controllers/my_script.php:xxx)</p>
<p>Filename: libraries/Session.php</p>复制代码

这个错误从字面理解,就是设置 header() 的时候发现 header 中已经有内容了,那么,在异常信息中, headers already sent by () 括号里的内容就很重要了,它代表了是那一行的输出致使了这个问题。按照定位的位置,是脚本中的一个printf语句;继续看,是 Session 中的 setcookie() 方法发现这个 printf 语句已经输出内容了。cookie

想要解决这个问题,可使用 sprintf 来组装字符串,使用 fwrite 等标准输出将内容输出到控制台。app

为何会出现 headers already sent

在 PHP 中,不能在header()以前 echo 任何内容,一旦 echo,PHP 会发送已有的 header 内容,咱们作一下实验。框架

在实验以前,你须要把php.ini中的 output_buffering 关闭或者设置一个很小的值。以后重启 php-fpm。curl

[PHP]
...
output_buffering = 3
...复制代码

这样设置代表输出的 buffer 不超过 3 个字符。

而后重现一下这个 bug:

<?php
public function test() {
  echo 'asd';
  header('a: b');
}复制代码

使用 curl 访问一下,返回的 HTTP body 是 asd 和一个 headers already sent 错误信息,curl -I http://localhost/test一下看看 header,发现 a: b 并无输出到 header 中。

echo 的内容超出了缓冲区限制的长度,便会做为 HTTP body 输出给 WEB 服务器。一旦 echo,PHP 输出 header 的任务就等于结束了,那么此时调用header()就会抛出 headers already sent 的错误。

修改一下代码:

<?php
public function test() {
  header('b: c');
  echo 'asd';
  header('a: b');
}复制代码

此时输出的 HTTP body 内容是相同的,可是 curl -I 看到的 header 中多了 b: c,说明 echo 以前的header()正确的输出了内容。

setcookie 方法也会发送 header:set-cookie: xxx,因此同样会引发这个问题。

在上面的例子中,咱们将 output_buffering 设置为 3,若是 echo 的内容小于 3,是不会引发问题的,由于缓冲区缓冲了 echo 的内容,会在 header 输出以后再输出缓冲内容。在实际的应用中,能够给 output_buffering 一个稍大一些的值。

可是,不能依赖 output_buffering 的大小,应该尽可能避免在业务代码中使用 echo 和 print 系函数。

怎样使用 echo

echo 很方便,古董 PHP 开发还会使用 echo 调试大法,并且咱们要输出 HTTP 内容确定要用到 echo 或者 print,怎么可能避免使用呢?

业务代码中尽可能避免

咱们应该避免在业务中使用,而不是禁止使用。当使用 echo 的时候,由于上述缘由出现 headers already sent 错误,要看 output_buffering 设置的大小和 echo 内容的长度,这给 debug 带来了很大的不肯定性,测试环境极可能会漏掉这个 case。

在业务中,可能用到 echo 的缘由有:1. 调试代码,查看变量;2. 命令行脚本的输出。对于 1,建议经过调试工具调试,或者使用插件 clockwork;对于 2,能够在脚本中经过标准输出来输出重要内容,并不须要使用 echo。

<?php
fwrite(STDOUT, $content);复制代码

若是基于某种缘由必定要使用,能够将一段输出用 ob_start 和 ob_end 包裹起来。被包裹的输出会进入内部缓冲区,在须要的时候再 flush 出来。

<?php
// ob_start 的函数定义
bool ob_start ([ callable $output_callback = NULL [, int $chunk_size = 0 [, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS ]]])复制代码

$chunk_size=0的时候,只有在关闭缓冲区的时候才会输出缓冲区的内容。[^3]

<?php
public function test() {
  ob_start(); // 打开缓冲区
  echo 'asd';
  header('a: b');
  ob_end_flush(); // 关闭缓冲区,将缓冲区的内容输出到 HTTP body
}复制代码

通常框架的输出都是这样设计的,echo 会包裹在 ob_start 和 ob_end 之间。

ob_start 的问题

ob_start 不能解决 PHP 代码不规范致使的 headers already sent:

<?php
public function test() {
  ob_start(); // 打开缓冲区
  echo 'asd';
  header('a: b');
  ob_end_flush(); // 关闭缓冲区,将缓冲区的内容输出到 HTTP body
}
// 这段代码也会报错复制代码

使用 ob_start 须要及时的将数据输出出去,不然可能会由于字符串拼接和二进制内容冲突:

<?php
public function test() {
  ob_start(); // 打开缓冲区
  echo 'asd';
  imagepng($resource);
  ob_end_flush(); // 关闭缓冲区,将缓冲区的内容输出到 HTTP body
}
// asd 和 imagepng() 的内容混在一块儿,输出的图片不可用复制代码

好的实践

综上所述,一个良好的实践是:

  • output_buffering 关闭或者设置一个较小的数值[^2]
  • 如非必要,不使用 echo 和 print 系函数
  • 使用 echo 时,尽可能用 ob_start 和 ob_end 包裹
  • 使用 ob_start 和 ob_end 包裹时,对本身包裹的内容有清晰的认识,尽可能不要跨函数使用 ob_start 和 ob_end

[^1]: 参见 stackoverflow 回答,除此以外,还有 UTF-8 BOM 等其余缘由
[^2]: 参见PHP程序访问报错Warning: Cannot modify header information - headers already sent byPHP: 运行时配置 - Manual,开启 output_buffering 可能影响 PHP 执行效率[^3]: 使用 ob_start 的时候不受 php.ini 中的 output_buffering 大小的影响

相关文章
相关标签/搜索