状況
深夜2時。リダイレクトが機能していない。ユーザーはダッシュボードの代わりに空白のページが表示されている。ログにはこう出ている:
Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/includes/config.php:1) in /var/www/html/login.php on line 45
このメッセージがほぼすべてを物語っている。PHPがheader()、setcookie()、またはsession_start()を呼び出そうとしたが、すでに何かがブラウザにバイトを送信していた。一度出力が送信されると、HTTPヘッダーはロックされる。それで終わりだ。
原因となるもの
原因は常に同じだ:ヘッダー呼び出しより前に何かが出力を生成している。厄介なのは、「出力」がほぼ目に見えない場合があることだ。
よくある原因:
- 開始タグ
<?phpの前にあるスペースや改行 - ファイル先頭のUTF-8 BOM(ほとんどのエディタでは不可視 — 3つのサイレントバイト:
EF BB BF) header()より前に呼ばれたechoやprint- リダイレクト前にPHPタグの外で出力されたHTML
- 出力が始まった後に呼ばれた
session_start() - 暗黙的に何かを出力する
includeやrequire
デバッグ手順
ステップ1:エラーメッセージをよく読む
PHPは出力がどこで始まったかを正確に教えてくれる。この部分に注目しよう:
(output started at /var/www/html/includes/config.php:1)
そのファイルのその行に移動しよう。そこが現場だ。
ステップ2:<?phpの前の空白を確認する
ファイルをhexエディタで開くか、次を実行する:
cat -A /var/www/html/includes/config.php | head -5
<?phpタグより前の行に^Mや$が見えるか?タグの前に何か文字がある?それが問題だ。不要なスペース1つですべてが壊れる。
ステップ3:UTF-8 BOMを確認する
次を実行する:
file /var/www/html/includes/config.php
出力にUTF-8 Unicode (with BOM)と表示されていれば、それが原因だ。その3つの不可視バイトが、PHPが開始する前に出力として送信される。
次のコマンドで削除する:
sed -i '1s/^\xEF\xBB\xBF//' /var/www/html/includes/config.php
またはVS Codeで開き、右下隅でエンコードをUTF-8 with BOMから通常のUTF-8に切り替える。
ステップ4:コード内の早まった出力を探す
ヘッダー呼び出しより前に現れるecho、print、またはHTMLを検索する:
grep -n 'echo\|print\|?>' /var/www/html/login.php
すべてを壊す典型的なパターンはこれだ:
<!-- bad: HTML before redirect -->
<html>
<head><title>Login</title></head>
<?php
session_start(); // 遅すぎる — すでにHTMLが送信済み
header('Location: /dashboard.php'); // これは失敗する
?>
解決策
修正1:すべてのヘッダーを出力より前に移動する
シンプルなルール:header()、setcookie()、session_start()は、空白行1つを含め、出力を生成するものより前に記述しなければならない。
<?php
// 正しい: sessionとheaderを最初に、この前には何もない
session_start();
if (!isset($_SESSION['user'])) {
header('Location: /login.php');
exit(); // リダイレクト後は常にexitする
}
?>
<!DOCTYPE html>
<html>
...
修正2:出力バッファリングを使用する
深夜2時にファイルを再構成するのはリスクが高い。出力バッファリングが時間を稼いでくれる:
<?php
ob_start(); // すべての出力をメモリに保持する
// echo/printはヘッダーを即座に送信しなくなる
echo 'something';
// これが機能するようになる
header('Location: /dashboard.php');
ob_end_flush();
exit();
?>
ob_start()はブラウザにフラッシュする代わりに出力をメモリに保持する。バッファがフラッシュされるまでヘッダーは変更可能なままだ。恒久的な修正ではないが、しっかりした応急処置になる。
php.iniで出力バッファリングをグローバルに有効にすることもできる:
output_buffering = 4096
その後、PHPプロセスを再起動する:
sudo systemctl restart php8.2-fpm
# または
sudo systemctl restart apache2
修正3:PHPの終了タグを削除する
閉じタグ?>の後の末尾改行は出力としてカウントされる。修正方法:それを削除する。閉じタグはPHPでは任意であり、純粋なPHPファイルでは省略することが推奨されている:
<?php
// 純粋なPHPファイル — 閉じ ?> は不要
define('DB_HOST', 'localhost');
define('DB_NAME', 'myapp');
// ファイル終端 — ?> なし
修正4:インクルードファイルに潜むBOMを探す
BOMはメインファイルにない場合もある。すべてのインクルードファイルを確認しよう:
find /var/www/html -name '*.php' | xargs grep -l $'\xef\xbb\xbf'
すべてのPHPファイルからBOMを一括削除する:
find /var/www/html -name '*.php' -exec sed -i '1s/^\xEF\xBB\xBF//' {} \;
修正の確認
壊れていたフローを直接テストする。リダイレクトの場合、curlでレスポンスヘッダーを確認する:
curl -I http://yoursite.com/login.php
正常に動作しているリダイレクトはこのように見える:
HTTP/1.1 302 Found
Location: /dashboard.php
まだLocationヘッダーなしの200 OKが表示されているか?どこかでまだ出力が漏れている。
すべての警告を表示するために、一時的に完全なエラーレポートを有効にする:
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
PHPが、不正な出力が発生している正確なファイルと行を教えてくれる。それを見つけて、修正して、完了だ。
得られた教訓
session_start()とheader()は絶対的な先頭に配置する — HTMLより前、echoより前、空白より前に。- PHPのみのファイルから閉じタグ
?>を削除する。これは発火を待つ空白の罠だ。 - エディタをBOMなしUTF-8で保存するよう設定する。UTF-8 BOMは不可視でサイレントであり、1時間のデバッグを消費させる。
- 開発中の安全網として、アプリのエントリーポイントで
ob_start()を使用する — 恒久的なアーキテクチャ上の決定としてではなく。 - エラーでインクルードファイルが指摘された場合、そのファイル内でPHPタグの外にある空白行1つでもこのエラーを引き起こすのに十分だということを忘れないように。

