
PHP 报错 “headers already sent” 是什么意思?常见原因与解决方法
在 PHP 程序中调用 header()、setcookie() 或启动 Session 时,可能看到类似这样的提示:
Warning: Cannot modify header information - headers already sent by
(output started at /path/to/index.php:3) in /path/to/login.php on line 12
这通常不是说 PHP 完全不能再执行,也不一定是 HTTP 请求本身出错,而是:程序已经开始向响应中写入内容,随后又试图设置 HTTP 响应头。响应头必须在响应正文开始发送前确定;一旦正文已经开始发送,服务器就无法可靠地把新的响应头插入已经发出的内容之前。
本文说明这个错误的机制、常见来源和定位办法,并分别介绍重定向、Cookie、Session 与输出缓冲的处理方式。示例面向常见 PHP Web 应用;命令行脚本的行为和 Web 请求不同,相关场景也会单独说明。
一、什么是响应头,为什么必须先发送
一次典型的 HTTP 响应包含状态行、响应头和响应正文。以浏览器访问页面为例,服务器可能返回:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Set-Cookie: session_id=...
<!doctype html>
<html>...</html>
Content-Type、Location、Set-Cookie 等属于响应头;HTML、JSON、图片数据等属于正文。PHP 通过 header() 设置响应头,通过 setcookie() 请求浏览器设置 Cookie。对于 PHP Session,常见配置下,session_start() 也需要在响应头尚可发送时运行,因为它可能需要发送或更新 Session Cookie。
因此,通常应按以下顺序处理请求:
- 读取请求并执行需要的业务判断。
- 设置状态码、Cookie、重定向等响应头。
- 输出 HTML、JSON 或其他正文。
容易出错的顺序则是:先输出页面内容,再设置响应头。这里的“输出”不只指显式调用 echo,也可能来自模板、文件开头的空格、UTF-8 BOM、被包含文件或运行时错误信息。
二、最常见原因:已经输出了正文
下面的代码先输出文字,再尝试重定向,通常会产生 “headers already sent” 警告:
<?php
echo '正在跳转……';
header('Location: /login.php');
exit;
应先完成重定向,再输出内容:
<?php
header('Location: /login.php', true, 302);
exit;
第二个参数表示是否替换已有的同名响应头;第三个参数指定状态码。对 HTTP 重定向,常见选择是 302(临时跳转)或 301(永久跳转),应根据业务语义选择。重定向后应调用 exit 或 die,避免脚本继续执行并输出原页面内容。
header('Location: ...') 并不会自动停止 PHP 脚本。即使重定向头成功设置,后续代码仍可能继续执行;只设置头而不结束执行,是常见的逻辑错误。
三、定位错误:看清“最先输出”的文件与行号
错误信息通常会指出两个位置:
output started at ...:PHP 认为输出开始的位置,通常是首先排查的地方。- 当前调用
header()、setcookie()等函数的位置:这里是尝试发送头信息的位置,不一定是根因。
例如,提示输出从 layout.php 第 1 行开始,而 login.php 第 20 行调用了 header(),就要先检查 layout.php 是否在顶部有空行、BOM 或直接输出了内容。包含文件可能比当前文件更早产生输出。
也可以用 PHP 的 headers_sent() 函数在代码中检查状态。它在响应头已发送时返回 true;可选参数会写入最早输出位置的文件名和行号:
<?php
$file = '';
$line = 0;
if (headers_sent($file, $line)) {
error_log("响应头已无法发送;输出位置:{$file}:{$line}");
} else {
header('X-App-Status: ready');
}
headers_sent() 适合诊断或在确有必要时避免重复设置响应头,但它通常不应被当作掩盖程序顺序错误的办法。若重定向头已经无法发送,简单跳过 header() 并不能完成重定向;应当修正输出时机。
如果开启了 PHP 输出缓冲,正文可能暂时还留在缓冲区中,尚未实际发送。此时 headers_sent() 可能仍返回 false。因此,函数检查反映的是“响应头是否已经发送”,不能用来保证程序中绝对没有产生过正文。
四、原因一:PHP 文件开头的空格、空行或 UTF-8 BOM
PHP 文件若在 <?php 之前含有空格、空行或其他文本,这些内容会被当作正文输出。特别是纯 PHP 文件,开头一个看似无害的换行,就可能让后续的 header() 失败:
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo 'OK';
上面的代码块中,若实际文件确实在 PHP 起始标签之前有换行,那么这个换行可能就是问题来源。编辑器可能会把它显示得不明显。
另一类隐蔽原因是 UTF-8 BOM(字节顺序标记)。某些编辑器会在 UTF-8 文件开头写入 BOM 字节;PHP 可能在解析 PHP 标签前就把它作为输出处理。可以在编辑器中将文件保存为“UTF-8 无 BOM”,并检查被包含的 PHP 文件是否也带有 BOM。
只包含 PHP 代码的文件,通常建议省略结尾的 ?>:
<?php
// 纯 PHP 逻辑
function get_status(): string
{
return 'ok';
}
省略结尾标签可以减少标签之后意外空白造成的输出风险。若文件中需要混写 HTML,则结尾标签是正常的;关键是确保在需要设置响应头之前没有输出。
五、原因二:包含文件或模板提前输出
业务代码可能自身没有 echo,但在调用 header() 前执行了 include 或 require。被包含文件中的 HTML、空白、调试输出或错误信息,同样属于当前响应的正文。
<?php
require __DIR__ . '/header.php'; // 这个模板可能输出了 HTML
if (!$loggedIn) {
header('Location: /login.php');
exit;
}
如果 header.php 是页面模板并输出了导航栏,上述顺序就不合适。应先做权限判断和重定向,再加载页面模板:
<?php
if (!$loggedIn) {
header('Location: /login.php', true, 302);
exit;
}
require __DIR__ . '/header.php';
把业务逻辑和页面呈现分开,有助于控制响应顺序:先处理请求和决定响应,再输出模板。调试时也要检查自动加载文件、公共配置文件和错误处理代码,因为它们可能间接包含模板或产生输出。
六、原因三:调试信息、警告或异常页面输出
var_dump()、print_r()、临时 echo 都会输出正文。开发时留下的调试语句,可能让登录重定向或下载响应失效。应删除这些输出,或改用日志:
<?php
error_log('正在处理用户 ID:' . $userId);
PHP 警告、通知或未捕获异常也可能显示在响应正文中。开发环境中将错误显示到页面,便于调试;生产环境通常应把错误记录到日志,而不是把错误详情输出给用户。这样既能降低敏感信息泄露风险,也能避免错误文本抢先成为正文。
关闭页面错误显示只能改变错误信息的呈现方式,不能修复程序本身。应同时检查错误日志、异常处理和错误处理配置,找出真正的异常或警告来源。
七、设置 Cookie 与启动 Session 的正确时机
setcookie() 需要通过响应头请求浏览器保存 Cookie,因此应在任何正文输出之前调用。示例:
<?php
$cookieOptions = [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
];
setcookie('theme', 'dark', $cookieOptions);
echo '偏好已保存';
此数组形式的选项适用于 PHP 7.3 及以上版本。secure => true 表示 Cookie 仅通过安全连接发送,适合 HTTPS 网站;若站点确实需要在 HTTP 下使用,就应根据部署环境处理,而不是照抄这个设置。httponly 限制浏览器脚本通过常见脚本接口读取 Cookie,但不能阻止所有攻击,也不意味着 Cookie 内容可以存放密码或其他明文敏感信息。samesite 影响跨站请求携带 Cookie 的行为;具体选择应结合登录、跨站集成和 CSRF 防护设计。
Cookie 的 path、域名、有效期与 SameSite 设置都有适用范围;它们不会让已经发送的响应头重新变得可修改。Cookie 也不是用来替代服务端授权检查的安全机制。
常见配置下,Session 启动需要读取或发送 Session Cookie,应在输出之前调用 session_start():
<?php
session_start();
$_SESSION['user_id'] = 42;
echo 'Session 已更新';
如果需要通过 session_set_cookie_params() 配置 Session Cookie,应在 session_start() 之前设置;同样要保证此前没有输出。具体可用参数和行为会受到 PHP 版本及 Session 配置影响,应以当前 PHP 版本的手册为准。
Session 不是只能从页面顶部启动,但必须在需要它且响应头仍可设置时启动。若程序采用统一入口,应在入口阶段完成 Session 初始化,再执行模板输出。也不要为了消除警告而无条件重复调用 session_start();应明确哪些组件负责启动 Session,并按项目结构统一管理。
八、输出缓冲:能缓解什么,不能保证什么
PHP 输出缓冲可以暂存正文,使 PHP 暂时不立即把内容发送给客户端。缓冲开启期间,程序通常仍有机会设置响应头:
<?php
ob_start();
echo '这段内容先进入缓冲区';
header('X-App-Status: ready');
$content = ob_get_clean();
echo $content;
缓冲的作用是延迟正文发送,不是把响应头放到已经发送的正文前面。如果缓冲区被刷新、超过配置限制后自动发送,或其他代码调用了 ob_flush()、flush(),响应头仍可能来不及设置。服务器、PHP SAPI 和中间层配置也会影响数据何时发出。
另外,ob_start() 不是修复所有 “headers already sent” 的通用方案。它可能让一个顺序错误暂时不再显现,却保留了重复输出、异常处理不清晰或内存占用过大的问题。大型响应、文件下载、流式输出和实时推送尤其需要谨慎:为了先缓冲完整内容,可能带来额外内存消耗,或破坏本来需要逐步发送数据的设计。
若确实需要缓冲,例如生成邮件正文、捕获模板输出或在验证完成前构造小型响应,应明确缓冲的开始、结束与异常清理路径。不要依赖某个环境“恰好开启了输出缓冲”来让程序正常工作。
九、常见错误做法与边界
1. 在错误发生后再调用 header()
如果正文已发送,后续调用不能撤回已发送的数据,也不能可靠地补上重定向或 Cookie。应将头信息调用移到所有输出之前,并检查最早输出的位置。
2. 认为 exit 能修复已经发生的输出
exit 能停止脚本继续运行,却不能倒转已经发出的正文。它应放在成功设置重定向之后,用来阻止后续逻辑执行,而不是代替正确的调用顺序。
3. 用 JavaScript 跳转冒充 HTTP 重定向
在页面中输出 JavaScript 改变浏览器地址,可能在某些界面场景中有用途,但它不是 HTTP 层的重定向,也不能替代状态码、响应头或访问控制。若请求应得到重定向响应,应在输出前正确设置 Location。
4. 不理解状态码就使用 301
301 通常表示永久重定向,浏览器和缓存可能长期记住结果;临时场景通常选择 302 或其他合适的临时重定向状态码。不要为了“更快”或“看起来正规”而把所有跳转都写成 301。
5. 把 headers_sent() 当作权限判断
它只检查响应头发送状态,与用户是否登录、是否有权限访问资源无关。访问控制应在执行敏感操作、输出数据或提供下载之前完成,不能用页面跳转代替服务端权限检查。
6. 把不可信输入直接拼进响应头
响应头的值不应直接由用户输入拼接而成。例如用请求参数构造 Location 时,若没有严格校验,可能造成不安全的外部跳转或其他头部处理问题。应优先使用服务端维护的允许列表或固定路由,并按业务需要校验目标地址。不要假设 PHP 会替业务自动完成所有安全校验。
对 header() 的行为和具体校验细节,应结合所用 PHP 版本查阅官方文档;无论运行环境如何,都不应依赖未验证的输入构造响应头。尤其是重定向目标,不应仅凭字符串开头看似可信就允许跳转。
7. 在输出后尝试改写正文类型
header('Content-Type: application/json') 应在输出 JSON 之前调用。仅仅把响应头设为 JSON,并不会把已输出的 HTML 转换成 JSON;同样,声明字符集也不会自动修正实际编码错误。
十、不同场景下的推荐写法
登录失败后重定向
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: /login.php', true, 302);
exit;
}
// 只有通过检查后才加载会输出内容的模板
require __DIR__ . '/account-page.php';
返回 JSON
<?php
header('Content-Type: application/json; charset=UTF-8');
$result = [
'ok' => true,
'count' => 3,
];
echo json_encode($result, JSON_UNESCAPED_UNICODE);
如果接口需要处理编码错误、异常或 HTTP 状态码,也应在输出 JSON 正文之前完成相应处理。不要先输出调试文字,再尝试切换响应类型。
下载文件
<?php
$file = __DIR__ . '/reports/report.csv';
if (!is_file($file) || !is_readable($file)) {
http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo '文件不存在';
exit;
}
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="report.csv"');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
下载场景尤其要避免在文件数据之前输出空格、调试信息或错误文本,否则下载内容可能被污染,或者相关响应头无法发送。示例中的文件名是固定值;若文件名来自用户输入,应进行严格校验,不能直接拼接进 Content-Disposition。
十一、命令行、代理与“已发送”的理解范围
“headers already sent”主要针对 PHP 生成 HTTP 响应的场景。在命令行运行 PHP 时,不存在浏览器接收的常规 HTTP 响应过程;某些与响应头有关的函数行为会因 SAPI 和运行方式而不同。不要据命令行下未出现警告,就认定 Web 请求中的调用顺序没有问题。
反过来,浏览器开发者工具中看到的响应头,也可能经过 Web 服务器、反向代理或 CDN 处理。PHP 能否设置响应头与这些组件最终如何转发或修改响应,是相关但不同的问题。排查 PHP 警告时,应先依据 PHP 给出的输出位置定位应用代码,再检查服务器层配置;不要把所有响应头异常都归结为 PHP 输出缓冲。
十二、实用排查清单
- 确认警告中的
output started at文件和行号。 - 检查该文件开头是否有空格、空行或 UTF-8 BOM。
- 检查该位置之前执行的
echo、模板输出、调试语句和文件包含。 - 确认
header()、setcookie()、session_start()位于正文输出之前。 - 若页面错误显示已开启,查看是否有警告或异常先被输出;结合日志判断根因。
- 核对重定向状态码和目标地址,并在成功设置跳转后停止脚本。
- 只有在确有需要时使用输出缓冲,并确认没有提前刷新缓冲区。
总结
PHP 的 “headers already sent” 核心原因是:响应正文已经开始发送,程序又试图设置响应头。最常见的来源包括提前输出的 HTML、调试信息、包含文件、PHP 文件开头的空白和 UTF-8 BOM。
解决问题时,先根据错误信息定位最早输出的位置,再把重定向、Cookie、Session 和内容类型等响应头操作移到正文输出之前。输出缓冲可以延迟正文发送,但不是修复错误调用顺序的万能办法。只要明确“先决定响应,再输出正文”的边界,大多数此类问题都能直接定位并避免。









