PAYATHON 2026

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()。

备注:内容仅供参考。