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 大小的影響

相關文章
相關標籤/搜索