Laravel 12 重定向为何返回 200 且缺少响应头?
结论:现有信息不能认定是 Laravel 12 的中间件配置问题
所示 Controller 返回 Socialite 重定向响应的写法没有明显问题。Laravel 12 的默认 web 中间件组仍包含 StartSession、EncryptCookies 和 AddQueuedCookiesToResponse,不能仅凭这些现象断定升级后需要手动补齐 session 中间件。Laravel 12 默认中间件配置
应先确认响应对象生成时是否已经包含 302 状态码和 Location 响应头,再查明它们在哪一层消失。优先检查 PHP 是否提前发送了响应头,同时对比源站与代理出口的响应。目前证据不足,还不能确定唯一根因。
为什么有重定向 HTML,却没有 Location?
Symfony 的 RedirectResponse 会生成 HTML 后备页面,同时设置 Location,默认状态码为 302。看到这段 HTML 说明重定向正文已经生成,但不能证明状态码和响应头也成功发送了。Symfony RedirectResponse 源码
一种可能是 PHP 在框架发送响应前已经输出内容,导致默认的 200 响应先被提交。此时,Symfony 的 sendHeaders() 遇到 headers_sent() 为 true 会直接返回,后续的 sendContent() 仍可能输出重定向 HTML,于是出现正文像重定向、状态码却是 200 且缺少响应头的情况。这种原因符合所描述的现象,但仍需验证。Symfony Response 源码
上述 Symfony 源码以 7.2 分支说明机制,实际项目应以 composer.lock 和已安装依赖为准。
1. 记录 Controller 实际返回的响应
临时修改 Controller,先记录状态,再返回原响应:
public function redirectToProvider()
{
$response = Socialite::driver('microsoft')->redirect();
$file = '';
$line = 0;
$sent = headers_sent($file, $line);
\Illuminate\Support\Facades\Log::debug('Microsoft redirect diagnostic', [
'response_class' => get_class($response),
'status' => $response->getStatusCode(),
'has_location' => $response->headers->has('Location'),
'headers_sent' => $sent,
'output_file' => $file,
'output_line' => $line,
]);
return $response;
}
这里只记录 Location 是否存在,不必记录完整 OAuth URL。
根据日志选择排查方向:
- 对象是
302,且存在Location:重定向已正确生成,继续检查后续中间件、PHP 发送阶段和代理。 - 对象已经是
200,或没有Location:检查 Socialite provider、相关扩展及自定义响应处理。 headers_sent为true:优先查明提前输出的来源。
Controller 中的日志记录的是当时的状态,并非最终响应状态。如果这里正常,而客户端仍收到异常响应,可以在覆盖该路由的外层全局中间件中,于 $next($request) 返回后再次记录相同信息,判断响应是否被后续处理替换。诊断日志应写入文件或其他日志服务,避免使用会输出到 HTTP 正文的日志通道。
2. 排查 PHP 提前输出
headers_sent($file, $line) 可以返回开始输出的文件和行号。响应头一旦发送,PHP 就不能再正常追加响应头。PHP headers_sent() 文档
重点检查:
- PHP 文件开头的 UTF-8 BOM,或
<?php之前的空白。 - 纯 PHP 文件末尾
?>之后的字符。 - 启动文件、Service Provider、自动加载文件中的
echo、print、var_dump()或调试输出。 - 直接显示在页面上的 PHP warning、notice 或启动错误。
- 提前刷新输出缓冲区的代码。
同时查看 PHP、PHP-FPM 或 Apache 的错误日志。如果返回的文件名为空,也要检查 PHP 启动阶段的错误。
本地 Laragon 正常不能完全排除这类问题,因为不同环境的输出缓冲、错误显示和 PHP 运行方式可能不同。同样,一次检查得到 headers_sent() === false,也不能证明之后不会发生提前输出。
3. 对比源站响应与代理出口
同一代理后的 Laravel 11 正常,可以作为对照,但两个域名仍可能命中不同的 VirtualHost、PHP-FPM pool、代理规则或缓存策略,因此还不能完全排除代理。
先检查公网入口的原始 GET 响应:
curl -sS -i https://mynewable.newable.dev/login/ms
不要加 -L,以免自动跟随跳转后混淆首个响应;也不要用 -I 代替 GET。
如果源站支持通过 HTTPS 直接访问,可以保留原域名和 TLS SNI,通过源站 IP 测试:
curl -sS -i \
--resolve mynewable.newable.dev:443:203.0.113.10 \
https://mynewable.newable.dev/login/ms
203.0.113.10 是示例地址,需要替换为实际源站 IP。如果源站只接受代理转发,应按实际内部协议和端口进行等价测试。
- 源站返回
302和Location,公网返回200:重点检查 Webscale、Apache 和域名对应的响应改写规则。 - 源站同样返回
200:继续检查应用响应处理、PHP 运行环境及源站 Apache 配置。
Server: Apache 和 X-WS-Origin: available 本身不足以确定哪一层改写了响应。
4. 确认实际生效的 web 中间件
检查路由展开后的中间件:
php artisan route:list --path=login/ms -vv
Laravel 12 的标准路由构建逻辑会将通过 withRouting(web: ...) 注册的路由放入 web 组。如果该路由确实位于所示 routes/web.php,正常情况下不需要再次手动添加整套 web 中间件。Laravel 12 路由构建源码
如果实际列表缺少 session 或 cookie 中间件,再检查是否有代码覆盖、移除或替换默认组。即便确实缺少 session 中间件,也需要继续查明为什么重定向状态码和 Location 同时丢失,不能只以 cookies 缺失作为结论。
TrustProxies 主要影响请求如何识别代理提供的协议、主机和地址等信息,不会替应用补发已经丢失的响应头。目标 OAuth URL 已正确生成时,继续调整该设置未必能解决当前异常。Laravel TrustProxies 源码
5. 用简单响应缩小范围
可以在同一个应用的 routes/web.php 中临时添加:
Route::get('/_diag/redirect', function () {
return redirect('/_diag/ok');
});
Route::get('/_diag/ok', function () {
return response()->json(['ok' => true]);
});
分别检查两条路由的状态码、Content-Type 和 Location:
- 普通重定向也异常:优先检查通用响应处理、提前输出和部署环境。
- 普通重定向正常,仅 Socialite 路由异常:检查该路由特有的中间件、provider 和执行路径。
- JSON 正文正常,但其响应头也不符合预期:进一步检查是否存在统一的响应发送或改写问题。
诊断结束后删除临时路由和日志。
修复后如何确认?
重新请求 /login/ms,确认首个响应返回预期的 302 和 Microsoft OAuth Location。如果流程采用 session 保存 OAuth state,还应验证 session cookie 的发送及回调阶段的 state 校验。
cookies 是否出现需要结合实际 session 配置判断。Strict-Transport-Security、X-XSS-Protection 等安全头也可能由代理或服务器添加,两个应用不一定完全一致。应根据响应对象日志、源站响应和代理出口响应之间的差异定位修复点,不要直接增加中间件或手工调用 header()。
备注:内容仅供参考。
