如何在PHP中修复“已发送标头”错误
如何在PHP中修复“已发送标头”错误
运行我的脚本时,我收到了几个类似于以下的错误:
警告:无法修改头信息——头信息已经在(/some/file.php:12)处开始输出,在 /some/file.php 的 23 行
错误信息中提到的行包含了 header()
和 setcookie()
调用。
造成这个问题的原因是什么?怎么解决?
如果在发送HTTP头之前发送了任何内容(如使用setcookie
或header
),就会触发此错误消息。在HTTP头之前输出东西的常见原因包括:
-
在文件的开头或结尾处意外添加的空白符,例如:
为了避免这个问题,可以简单地省略结束标记?>
- 它本来就不是必需的。
- 在PHP文件开头处添加了字节顺序标记的情况。可以使用十六进制编辑器检查PHP文件是否存在此问题。这些文件应该以字节
3F 3C
开头。可以安全地从文件开头删除BOMEF BB BF
。 - 显式输出,比如
echo
、printf
、readfile
、passthru
、之前的代码等。
- PHP发出的警告,如果设置了display_errors php.ini属性。PHP在程序员犯错时不会崩溃,而是悄悄地修复错误并发出警告。虽然可以修改
display_errors
或error_reporting配置,但最好解决问题。
常见原因是访问未定义的数组元素(如未使用empty
或isset
测试输入是否设置即使用$_POST['input']
)或使用未定义的常量而不是字符串字面量(如在$_POST[input]
中,缺少引号)。
开启输出缓冲应该能解决这个问题。在调用ob_start
之后,所有输出都缓存在内存中,直到释放缓冲区,例如使用ob_end_flush
函数。
然而,虽然输出缓冲可以避免这些问题,但是你应该真正地确定为什么你的应用在 HTTP 标头之前输出了一个 HTTP 主体。这就像在告诉发起者拨错号码之前,先接电话并讨论你的一天和天气。
在发送标头之前没有输出!
发送/修改HTTP标头的函数必须在任何输出之前调用。
总结 ⇊
否则调用将失败:
警告:无法修改标头信息 - 标头已被发送(输出开始于脚本:行)
一些修改HTTP标头的函数包括:
输出可能是:
-
无意:
之前或
?>
之后的空格- UTF-8字节顺序标记特别是
- 以前的错误消息或通知
-
有意:
print
、echo
和其他产生输出的函数- 原始的
部分优先于
代码。
为什么会发生这种情况?
要理解为什么必须在输出之前发送标头,有必要看一下典型的HTTP响应。PHP脚本主要生成HTML内容,但也传递一组HTTP/CGI标头至Web服务器:
HTTP/1.1 200 OK Powered-By: PHP/5.3.7 Vary: Accept-Encoding Content-Type: text/html; charset=utf-8PHP page output page Content
Some more output follows...
and
页面/输出总是遵循页头。PHP必须首先将页头传递给Web服务器。它只能执行一次。在双重换行符之后,它再也无法修改它们。
当PHP收到第一个输出(print
,echo
,)时,它将刷新所有收集的标头。之后,它可以发送所有所需的输出。但是此时无法发送进一步的HTTP标头。
如何查找过早输出发生的位置?
header()
警告包含定位问题原因的所有相关信息:
警告:无法修改标题信息-标题已由(输出开始于/www/usr2345/htdocs/auth.php:52)在/www/usr2345/htdocs/index.php的100行处
这里的“第100行”是指header()
调用失败的脚本。
圆括号内的“输出开始于”注释更重要。它标识了先前输出的源。在这个例子中,它是auth.php
和 52
行。这就是您必须寻找过早输出的位置。
典型的原因:
-
打印,回声
print
和echo
语句中的有意输出将终止发送HTTP标头的机会。必须重新构建应用程序流程以避免此问题。使用函数和模板方案。确保在编写消息之前调用header()
。产生输出的函数包括:
print
、echo
、printf
、vprintf
trigger_error
、ob_flush
、ob_end_flush
、var_dump
、print_r
readfile
、passthru
、flush
、imagepng
、imagejpeg
还有其他许多用户定义的函数。
-
原始HTML区域
在一个
.php
文件中未处理的HTML段也会被直接输出。
必须注意会触发header()
调用的脚本条件,
在任何原始的块前。
使用模板方案将处理和输出逻辑分离。
- 将表单处理代码置于脚本顶部。
- 使用临时字符串变量来推迟消息。
- 实际的输出逻辑和夹杂在其中的HTML输出应该放在最后。
-
前的空格用于"script.php line 1"警告
如果警告涉及内联
1
输出,则其主要是
在开头的标记之前的空格、文本或HTML。
同样也可能出现在附加的脚本或脚本区段中:
?>
PHP实际上会吞掉关闭标签后的单个换行符。但是它不会
弥补多个换行符或制表符或空格挤入这样的间隙。 -
UTF-8 BOM
仅有换行符和空格可能会成为问题。但还有一些“隐形”的
字符序列会导致这种问题。最著名的是
UTF-8 BOM(字节顺序标记)
它在大多数文本编辑器中不会显示出来。它是字节序列EF BB BF
,对于UTF-8编码的文档来说是可选的和多余的。然而,PHP必须将其视为原始输出。它可能会显示为ï» ¿
字符在输出中(如果客户端将文档解释为Latin-1)或类似的“垃圾”。特别是图形编辑器和基于Java的集成开发环境(IDE)对其存在毫不知情。它们不会可视化它(受Unicode标准的规定)。然而,大多数程序员和控制台编辑器会识别它:
在这里,很容易早期识别问题。其他编辑器可能会在文件/设置菜单中识别其存在(Windows上的Notepad++可以识别和解决该问题),检查BOM的存在的另一个选项是使用十六进制编辑器。在*nix系统中,
hexdump
通常可用,如果没有图形变体,则简化了审计这些和其他问题:简单的解决方法是将文本编辑器设置为保存文件为"UTF-8(没有BOM)"或类似的命名规范。否则,新手可能会创建新文件,然后将先前的代码复制并粘贴回去。
更正工具
还有自动化工具用于检查和重写文本文件(
sed
/awk
或recode
)。特别针对PHP,有phptags
标签清理程序。它将关闭和打开标记重写为长和短形式,但也容易修正前导和尾随空格,Unicode和UTF-x BOM问题:phptags --whitespace *.php
可以安全地用于整个包含或项目目录。
-
?>
后面的空格如果错误来源被提及为在
结束标记?>
之后,
那么就是在这里某些空格或原始文本被写出。PHP结束标记并不在此终止脚本执行。
在此之后的任何文本/空格字符都将作为页面内容继续写出。通常建议,特别是对新手来说,应该省略末尾的
?>
PHP关闭标签,
这种做法可以避免一小部分这种情况。(很常见的是include()
脚本是罪魁祸首。) -
错误来源被提及为“第0行未知”
如果没有具体的错误来源,那么通常是PHP扩展或php.ini设置。
- 偶尔是
gzip
流编码设置
或ob_gzhandler
。 - 但也可能是任何双重加载的
extension=
模块
产生隐式的PHP启动/警告消息。
- 偶尔是
-
先前的错误消息
如果另一个PHP语句或表达式导致警告消息或
注意事项被打印出来,也算是过早的输出。在这种情况下,您需要避免错误,
延迟语句执行,或使用例如isset()
或@()
抑制消息-当两者都不会妨碍以后的调试时。没有错误信息
如果您在
php.ini
中禁用了error_reporting
或display_errors
,则不会显示警告。但忽略错误并不能解决问题。在输出之后仍然不能发送头信息。因此,当
header("Location: ...")
重定向失败时,建议检查警告信息。通过在调用脚本上方添加两个简单的命令来重新启用它们:error_reporting(E_ALL); ini_set("display_errors", 1);
如果其他方法都无效,您还可以使用
set_error_handler("var_dump")
。说到重定向头信息,您应该经常使用这样的惯用语:
exit(header("Location: /finished.html"));
最好甚至用一个实用函数,打印用户消息以处理
header()
的失败。输出缓冲区作为解决办法
PHP 的输出缓冲是一种缓解此问题的解决办法。它通常可靠地工作,但不应代替适当的应用程序结构,并将输出与控制逻辑分开。它的实际目的是最小化分块传输到 Web 服务器。
-
output_buffering = 设置仍然有所帮助。
可以在 php.ini
或通过 .htaccess
或现代 FPM/FastCGI 设置下的 .user.ini 中进行配置。
启用它将允许 PHP 缓冲输出而不是立即将其传递给 Web 服务器。PHP 因此可以聚合 HTTP 头信息。 -
它也可以在调用脚本的顶部使用
ob_start();
来启用。但出于多种原因,这种方法不太可靠:-
即使
开始了第一个脚本,空格或 BOM 可能也会被打乱,使它无效。
-
它可能会隐藏 HTML 输出中的空格。但是,一旦应用逻辑尝试发送二进制内容(例如生成的图像)时,缓冲的额外输出就成为一个问题。(需要
ob_clean()
作为另一个解决方法。) -
缓冲区的大小是有限的,当保持默认值时,它很容易超限。这种情况也并不罕见,一旦出现就难以追踪原因。
-
因此,这两种方法可能变得不可靠-特别是在切换开发设置和等环境时。这就是为什么输出缓冲区通常被认为只是一种临时解决方法 / 严格的解决方法。
请参见手册中的 基本用法示例,以及更多的优缺点:
但是在其他服务器上它有效吗?
如果您以前没有收到过头部警告,则 输出缓冲php.ini设置 已更改。当前/新服务器上可能未配置它。
使用
headers_sent()
检查你总是可以使用
headers_sent()
来探测是否还有可能发送头部。
这对于有条件地打印信息或应用其他回退逻辑非常有用。if (headers_sent()) { die("Redirect failed. Please click on this link: "); } else{ exit(header("Location: /user.php")); }
有用的回退解决方法包括:
-
HTML
标签
如果你的应用程序构造上很难解决,那么一种简单(但有点不专业)的方法是注入HTML
标签来允许重定向。可以通过以下方式实现重定向:
或者短暂延迟后:
当在
部分之后使用时,这将导致非有效的HTML。大多数浏览器仍然接受它。
-
JavaScript 重定向
作为替代方案,可以使用JavaScript 重定向来进行页面重定向:
虽然这通常比
回退方法更符合HTML规范,但它依赖于能够运行JavaScript的客户端。
然而,在真正的HTTP header()调用失败时,这两种方法都能成为可接受的回退方法。理想情况下,你应该始终将这与一个用户友好的提示信息和可点击的链接结合起来作为最后的手段。(例如,这就是http_redirect() PECL扩展所做的。)
setcookie()
和session_start()
为什么也受到影响无论是
setcookie()
还是session_start()
都需要发送一个Set-Cookie:
HTTP头部。
因此,相同的条件适用,并且出现类似的错误消息来指示过早的输出情况。当然,它们还受到浏览器中禁用 cookies 以及代理问题的影响。会话功能显然也取决于空闲磁盘空间和其他 php.ini 设置等。
进一步的链接
- Google提供了相似讨论的冗长列表。
- 当然,Stack Overflow也涵盖了许多具体案例。
- WordPress FAQ以通用的方式解释了如何解决已发送标头警告问题?。
- Adobe社区:PHP 开发:为什么重定向不起作用(标头已发送)
- Nucleus FAQ:什么是“页面标头已发送”的意思?
- 其中更详细的解释是由 NicholasSolutions 编写的HTTP Headers 和 PHP header() 函数:NicholasSolutions 的教程(Internet Archive 链接)。
它详细介绍了 HTTP 并提供了一些重写脚本的指导方针。
-