PHP 以 CGI 模式運行時,會自動在腳本輸出前發送 Content-Type: text/html 標頭。當開發者同時使用 header()、setcookie() 或擴展模組也輸出標頭時,導致「Headers already sent」錯誤或標頭重複。本文分析機制並提供三種解決方案。
# CGI 模式典型輸出流程
1. Web Server 啟動 php-cgi 進程
2. php-cgi 執行腳本
3. 腳本首次輸出 (echo/print/HTML) 前
4. PHP 內核自動發送: Content-Type: text/html\r\n\r\n
5. 後續 header()/setcookie() 失效 (標頭已發送)
擴展模組副作用**:某些擴展 (如舊版 GD、PDFlib) 在 RINIT 階段輸出標頭/資料,導致標頭提前發送。
; php.ini
; 全部註解掉
;extension=php_mysql.dll
;extension=php_gd.dll
;extension=php_pdf.dll
; ...
; 需要時在腳本動態載入
<?php
if (need_gd()) {
dl('php_gd.dll'); // PHP 5.3+ 已移除 dl(),僅限舊版
// 使用 GD 功能
}
?>
缺點**:dl() 已在 PHP 5.3 移除 (Windows 除外)、效能差、執行緒不安全、現代 PHP 完全不適用。
# CGI 啟動參數
php-cgi -q script.php
# 或 shebang
#!/usr/bin/php -q
<?php ... ?>
致命副作用**:header()、setcookie()、session_start() 完全失效,**PHP Web 功能幾乎廢除**。僅適合純 CLI 腳本。
; php.ini
output_buffering = 4096 ; 或 On
; 或腳本開頭
<?php
ob_start(); // 手動開啟緩衝
?>
原理**:所有輸出 (含隱性標頭) 先存入記憶體緩衝區,腳本結束或 ob_end_flush() 時一次性發送,header() 可在任意位置調用。
#!/bin/sh
# php-cgi-wrapper
export PHP_FCGI_CHILDREN=4
export PHP_FCGI_MAX_REQUESTS=500
exec /usr/bin/php-cgi -b 127.0.0.1:9000
搭配 Nginx fastcgi_pass 127.0.0.1:9000; 使用 FastCGI 模式,**完全替代 CGI**,效能與穩定性大幅提升。
# 檢查檔案開頭是否有 BOM 或空白
hexdump -C script.php | head -20
# 正確: 3c 3f 70 68 70 (
使用編輯器「儲存為無 BOM UTF-8」,確保 為檔案最開頭。
棄用 CGI,採用 PHP-FPM + Nginx/Apache Event MPM
模式 進程模型 標頭處理 效能 狀態 CGI 請求 fork 新進程 自動輸出,易衝突 極差 淘汰 FastCGI (php-cgi -b) 常駐進程池 開發者控制 良好 過渡 PHP-FPM (php-fpm) 主進程管理 Worker 池 完整控制、緩衝、統計 優秀 標準 Apache mod_php 嵌入 Apache 進程 共享記憶體、較難隔離 良好 傳統 Swoole/OpenSwoole 協程異步非阻塞 完全程式化控制 極致 高併發專用
# 1. 確認運行模式
php -v | grep -i cgi/fcgi/fpm
# 2. 檢查 output_buffering
php -i | grep output_buffering
# 3. 測試 header 是否生效
# curl -I https://localhost/test.php | grep X-Test
# 4. 定位「Headers already sent」來源
# 錯誤訊息會顯示: output started at /path/file.php:12
# 檢查該檔案第 12 行前是否有輸出/BOM/空白