根本原因是调用$response->header()的时机或对象错误:必须在$next($request)返回响应实例后、return前设置,且不能对请求对象或null操作;psr-7不可变特性要求用withheader()或显式赋值,options请求需前置拦截并单独设头,中间件注册顺序不当也会导致头被覆盖。

中间件里写 $response->header() 却没出现在响应头中
根本原因是:你调用 $response->header() 的时机或对象错了。ThinkPHP 的响应对象是链式构建、最终统一发送的,中间件里拿到的 $response 必须是 $next($request) 返回的那个实例,且必须在 return 之前设置头 —— 任何在 $next() 之前、或对错误对象(比如请求对象、空值、null)调用 header(),都无效。
常见错误包括:
- 把
$response->header()写在$next($request)调用前,此时$response还不存在 - 漏了
return $response,导致 PHP 返回null,框架丢弃所有 header 设置 - 误把
$request->header()当成响应头操作(那是读请求头) - 在多个中间件中重复操作同一个
$response,后执行的中间件覆盖了前面设的头
为什么 return $next($request)->header(...) 也不生效
因为 $next($request) 返回的是一个 Response 实例,但它的 header 是只读副本;直接链式调用 ->header(...) 并不会修改原对象,也不会触发响应输出。TP6 的 Response::header() 是「返回新实例」的设计(PSR-7 不可变特性),所以必须显式赋值或链式 return:
- ❌ 错误:
return $next($request)->header([...]);(返回的是新 Response,但没被接收) - ✅ 正确:
$response = $next($request); $response->header([...]); return $response; - ✅ 或更简洁:
return $next($request)->withHeader('X-Foo', 'bar');(withHeader()才是 PSR-7 标准的不可变写法)
注意:Response::header() 是 ThinkPHP 封装方法,内部会调用 withHeader(),但它不是链式返回 —— 所以不能靠链式调用传递。
OPTIONS 预检请求下 $response->header() 完全不执行
这是最常被忽略的场景:浏览器发 OPTIONS 请求时,如果你的中间件没拦截它,而是放行给后续逻辑,那很可能走到控制器或 404,根本不会执行到你设 header 的那行代码。
必须在中间件开头显式判断并短路:
- 加
if ($request->isOptions()) { return response('', 204); } - 这个
response('', 204)返回的也是Response实例,你得立刻对它设 header,否则 204 响应里没有 CORS 头,预检仍失败 - 正确写法:
return response('', 204)->header([...]);或先赋值再设头
漏掉这步,前端控制台只会显示 “CORS error”,连 Network 面板都看不到响应头 —— 因为请求根本没进你的 header 设置逻辑。
中间件注册顺序导致 header 被覆盖
ThinkPHP 中间件是按数组顺序执行的:先注册的先执行(进入时),后注册的后执行(返回时)。而 $response->header() 是在 $next($request) **之后**才调用的,所以「谁在最后 return $response」,谁的 header 就生效。
典型冲突:
- 你在
Cors.php里设了Access-Control-Allow-Origin - 但后面还挂了
SessionInit或LoadLangPack中间件,它们也调用了$response->header() - 结果 Session 中间件覆盖了 Origin 头,或者语言中间件清掉了自定义头
解决办法:\think\middleware\Cors::class 或你自定义的 CORS 中间件,必须放在 app/middleware.php 数组的**最后一位**(即最外层),确保它是最后一个修改 $response 的中间件。
复杂点在于:一旦用了 Swoole 或多应用模式,中间件加载路径、命名空间、执行栈都会分层,app/middleware.php 里的顺序只影响当前应用入口 —— 别忘了检查子应用目录下的同名文件是否覆盖了它。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











