headers_sent($file, $line) 可精准定位首处输出位置;需检查所有文件为 utf-8 without bom、末尾无空行/空格、删除 ?> 标签,并排查 config.php 等被引入文件的隐式输出。

用 headers_sent() 精准定位输出起点
报错信息里写的 output started at /path/to/file.php on line 12 并不可靠——它只告诉你“头已发”,不告诉你“谁先动的手”。真正能抓到第一处输出位置的,只有 headers_sent($file, $line) 的第二个用法。
在疑似出问题的脚本最开头(<?php 后立即)加这三行:
if (headers_sent($file, $line)) {
error_log("Headers already sent in {$file} on line {$line}");
exit('Headers sent early — check above');
}
它会把首次输出的文件和行号写进 PHP 错误日志,比浏览器看到的报错更准。注意:$file 可能为空(比如因 PHP 启动错误提前输出),这时要查 error_log 配置路径和 display_errors 是否关闭。
检查所有 include/require 文件的末尾和编码
90% 的隐性输出藏在被引入的文件里:配置文件、函数库、公共模板片段。它们往往没被你盯紧,但一个空行、一个 BOM、一个 ?> 后的换行,就足以让整个请求崩掉。
- 用 VS Code 或 Notepad++ 打开每个
include文件,右下角确认编码是 UTF-8 without BOM,不是 “UTF-8”(带 BOM 的那个) - 文件末尾不能有多余空行或空格,
?>标签建议直接删掉——PHP 文件末尾不需要闭合标签,留着反而容易误加空格 - 检查
require_once 'config.php';这类语句,config.php里哪怕只有一行<?php $db_host = 'localhost';,结尾也不能有回车
ob_start() 不是万能解药,但能帮你过渡排查
加 ob_start() 在入口文件第一行确实能让 header() 暂时不报错,但它只是把问题掩盖了:输出还在,只是被缓存了。一旦你忘了 ob_end_flush() 或 ob_end_clean(),页面可能空白或乱码;更麻烦的是,它会让真实输出延迟,导致调试时看不出哪段代码真正在输出。
所以建议只在临时验证阶段用:
- 仅在
index.php或框架入口文件顶部第一行加ob_start(); - 不要在任意函数里嵌套调用
ob_start(),除非你明确配对了ob_end_clean() - 启用后仍要跑一遍
headers_sent()检查,否则你以为修好了,其实只是缓冲住了错误
CLI 和 Web 环境混用时的坑
如果你的代码既跑在 Web 请求里,也跑在 CLI(比如定时任务、队列消费者)里,header() 在 CLI 下根本无效,且不会报错——它只是静默失败。这时候如果逻辑没做环境判断,就会出现“本地测试跳转正常,线上 Cron 里执行却没反应”的情况。
安全做法是显式判断运行模式:
if (php_sapi_name() === 'cli') {
// CLI 下走日志或命令行提示
echo "Redirecting to /login.php\n";
} else {
if (!headers_sent()) {
header('Location: /login.php');
exit;
} else {
echo '<script>location.href="/login.php";</script>';
exit;
}
}
别依赖 $_SERVER['HTTP_HOST'] 是否存在来判断,它在某些 FastCGI 配置下可能被清空;php_sapi_name() 才是唯一可靠依据。
真正难的不是找到那一行空格,而是所有被 include 的文件都得满足“零输出”——连一个看不见的 BOM 都不行。一旦漏掉一个,整个链路就断了。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











