HTTP 100 Continue预加载失败排查及Symfony headers_sent重置问题
问题描述
我尝试利用HTTP 100 Continue配合preload头实现浏览器资源预加载,但未得到预期结果。以下是我的示例代码:
header('HTTP/1.1 100 Continue'); header('Link: </assets/style.css>; rel=preload'); header('Link: </assets/app.js>; rel=preload'); header(''); // send empty line to end 100 continue section flush(); sleep (4); // Emulate some processing while browser is preloading assets header('HTTP/1.1 200 OK'); echo "aaa "; echo "bbb "; flush(); exit;
预期返回的响应:
HTTP/1.1 100 Continue Link: </assets/style.css>; rel=preload Link: </assets/app.js>; rel=preload HTTP/1.1 200 OK aaa bbb
但实际返回的响应:
HTTP/1.1 200 OK Date: Wed, 04 Oct 2023 15:21:24 GMT Server: Apache/2.4.57 (Ubuntu) Link: </assets/app.js>; rel=preload Upgrade: h2,h2c Connection: Upgrade Vary: Accept-Encoding Transfer-Encoding: chunked Content-Type: text/html; charset=UTF-8 aaa bbb
我的疑问:
- 是否遗漏了什么?仅使用
flush()是否不足以发送HTTP 100 Continue头? - 当前技术栈为
apache+php-fpm,测试命令为curl -i https://localhost/。 - 此外,我将在Symfony项目中使用该功能,需在发送
100 Continue段后重置headers_sent(),否则Symfony的发送函数会因检测到headers_sent()而直接返回,但我不清楚如何在首次flush后重置它。
解决方案
一、HTTP 100 Continue未正确发送的原因及修复
在Apache + PHP-FPM架构下,你的实现存在几个关键问题:
- 触发逻辑错误
HTTP 100 Continue是客户端先发送Expect: 100-continue请求头,服务器才会返回100 Continue响应。你用curl -i测试时未携带该请求头,所以Apache/PHP-FPM不会返回100 Continue。测试需改用:
curl -i -H "Expect: 100-continue" --http1.1 https://localhost/
- 输出缓冲机制拦截
PHP-FPM默认开启output_buffering,加上Apache自身的缓冲,单纯flush()无法立即发送headers。需先关闭PHP输出缓冲:
// 放在所有输出之前执行 ob_end_flush(); ob_implicit_flush(true);
- 空行发送方式错误
用header('')无法生成100 Continue所需的空行,PHP的header()仅用于发送HTTP头,正确做法是直接输出空行并flush:
echo "\r\n"; flush();
- HTTP/2或压缩模块干扰
若Apache开启mod_http2或mod_deflate,会强化缓冲机制,导致100 Continue无法及时发送。测试时可强制用HTTP/1.1(如上述curl命令的--http1.1参数),或临时关闭压缩。
修正后的测试代码:
ob_end_flush(); ob_implicit_flush(true); // 仅当客户端发送Expect:100-continue时才返回100 Continue if (isset($_SERVER['HTTP_EXPECT']) && strtolower($_SERVER['HTTP_EXPECT']) === '100-continue') { header('HTTP/1.1 100 Continue'); header('Link: </assets/style.css>; rel=preload'); header('Link: </assets/app.js>; rel=preload'); echo "\r\n"; flush(); } sleep(4); header('HTTP/1.1 200 OK'); echo "aaa\r\nbbb\r\n"; flush(); exit;
二、Symfony中headers_sent()的处理方案
headers_sent()是PHP内置函数,检测的是HTTP头是否已发送到客户端,这个状态无法被重置——一旦头被发送,就无法收回。
在Symfony中,推荐使用StreamedResponse来控制输出流程,避免与框架的响应生命周期冲突:
use Symfony\Component\HttpFoundation\StreamedResponse; $response = new StreamedResponse(function () { ob_end_flush(); ob_implicit_flush(true); // 发送100 Continue(仅当客户端请求时) if (isset($_SERVER['HTTP_EXPECT']) && strtolower($_SERVER['HTTP_EXPECT']) === '100-continue') { header('HTTP/1.1 100 Continue'); header('Link: </assets/style.css>; rel=preload'); header('Link: </assets/app.js>; rel=preload'); echo "\r\n"; flush(); } // 模拟业务处理 sleep(4); // 发送最终响应 header('HTTP/1.1 200 OK'); echo "aaa\r\nbbb\r\n"; flush(); }); return $response;
StreamedResponse允许你逐步输出内容,完全掌控头和内容的发送时机,不会触发Symfony的headers_sent()检测拦截。
内容的提问来源于stack exchange,提问作者HubertNNN
相关产品推荐
相关产品推荐

