Warning: Cannot modify header information - headers already sent by (output started at ...)
原因:HTTP 協定規定 Header 必須在 Body 之前發送。任何輸出(echo、print、HTML、空白、BOM、錯誤訊息、var_dump)都會導致 PHP 發送 Header 結束標記並開始傳送 Body,此後再呼叫 header()、setcookie()、session_start() 即觸發警告。
| 場景 | 範例 | 檢查重點 |
|---|---|---|
| 檔案開頭有空白/BOM | <?php 前有換行、空格、UTF-8 BOM | 用十六進位編輯器/IDE 確認檔案以 3C 3F 70 68 70 ( |
| include/require 檔案輸出 | 配置檔末尾有 ?> 後空白、錯誤訊息 | 純 PHP 檔案省略結束標籤 |
| 錯誤/警告輸出 | Warning: mysqli_connect()... 在 header() 前 | 開發環境 display_errors=On,生產 Off + 記錄日誌 |
| 隱含輸出 | var_dump()、print_r()、die() 字串 | 除碼後移除或改用 logger |
| Session/cookie 在輸出後 | HTML 輸出後才 session_start() | 移至腳本最頂端 |
| 多重 include 鏈 | A.php include B.php,B.php 有輸出 | 追蹤 output started at 指向的檔案:行號 |
?>try-catch + Logger,關閉 display_errors<?php
// 入口檔最開頭(在任何輸出前)
ob_start(); // 開啟輸出緩衝,所有 echo 暫存記憶體
// ... 正常代碼 ...
echo '內容';
header('Location: success.php'); // 此時仍可修改 header
setcookie('token', $value);
session_start();
// 腳本結束時自動刷新緩衝,或手動
ob_end_flush();
注意:
ob_start() 會增加記憶體用量(大輸出時注意)ob_start() 需配對 ob_end_flush()/clean()flush()、Web Server 緩衝滿); php.ini
output_buffering = 4096 ; 4KB 內部緩衝,或 On 表示無限
; 重啟 PHP-FPM/Apache 生效
優缺點:一勞永逸,但隱藏了架構問題(輸出順序錯誤),除錯時較難發現過早輸出。
<?php
// Controller / Action
function loginAction(): void {
// 1. 純邏輯:驗證、設定 Session、Cookie、資料庫
if ($valid) {
$_SESSION['user'] = $user;
setcookie('remember', $token, time()+86400, '/', '', true, true);
// 2. 回應物件(不直接輸出)
return new RedirectResponse('/dashboard');
}
// 3. 失敗回應
return new ViewResponse('login.php', ['error' => '帳號密碼錯誤']);
}
// Front Controller / Router
$response = $router->dispatch($request);
// 統一發送 Header + Body
$response->send();
採用 PSR-7 Response 介面(Guzzle, Symfony HttpFoundation, Laravel Response, Slim 等),徹底消除「何時輸出」的時序耦合。
<?php
// 臨時加在懷疑位置,追蹤輸出來源
function debugOutputStarted(): void {
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
error_log('Output started trace: ' . json_encode(array_map(fn($f) => [
'file' => $f['file'] ?? 'unknown',
'line' => $f['line'] ?? 0,
'function' => $f['function'] ?? 'unknown',
], $trace)));
}
// 或使用 headers_sent() 檢查
if (headers_sent($file, $line)) {
error_log("Headers already sent in $file:$line");
}
output started at 指向的檔案:行號header()/session_start()/setcookie() 前有輸出?