Webman CORS中间件默认不暴露自定义响应头,需在config/middleware.php中显式配置expose_headers数组(如['content-disposition', 'x-request-id'])才能使前端通过response.headers.get()获取;同时路由须用Route::any()支持OPTIONS预检。

Webman 的 CORS 中间件默认不暴露自定义响应头,前端用 fetch 或 axios 拿不到 Content-Disposition、X-Request-ID 这类字段——不是后端没设,是浏览器压根不放行。
为什么设置了 Access-Control-Expose-Headers 却没生效
Webman 官方 cors 插件(webman/cors)的中间件默认只暴露基础头,比如 Content-Type,但不会自动包含你手动加的自定义头。即使你在控制器里写了:
return json(['code' => 0])->withHeader('Content-Disposition', 'attachment; filename="data.xlsx"');
前端依然读不到 response.headers.get('content-disposition'),因为浏览器被 CORS 策略拦住了。
常见错误现象:
- 控制台没报跨域错误,但
response.headers.get()返回null - 后端日志确认 header 已写入,但前端调试工具 Network 面板里 Response Headers 区域看不到该字段
- 用 Postman 或 curl 能看到 header,浏览器里却消失
如何正确配置 webman/cors 暴露自定义 Header
必须显式传入 expose_headers 参数,且注意大小写和拼写——浏览器对 header 名是大小写不敏感的,但中间件配置必须跟实际设置的完全一致(通常推荐小写)。
修改 config/middleware.php 中的 CORS 配置:
[\webman\middleware\Cors::class => [
'allow_origin' => ['http://localhost:3000'],
'allow_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
'allow_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'expose_headers' => ['content-disposition', 'x-request-id', 'x-total-count'], // ← 关键:这里要填小写、逗号分隔
'max_age' => 1800,
]]
说明:
-
expose_headers是数组,值为字符串,内容是你希望前端能读取的响应头名(不带Access-Control-前缀) - 若用
*通配符(如['*']),Webman 的Cors中间件不支持——它底层不走gorilla/handlers那套,不能动态暴露所有头 - 如果后端同时用了 Nginx,确保 Nginx 没过滤掉这些 header(检查是否有
underscores_in_headers off或proxy_hide_header)
Route::post() 导致预检失败的隐藏坑
Webman 中若只注册了 Route::post(),浏览器发复杂请求(如带 Content-Type: application/json)时会先发 OPTIONS 预检,但该路由未匹配到任何 handler,直接 404,CORS 失败。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
解决方案只有两个:
- 把路由改成
Route::any(),让中间件能拦截并响应OPTIONS - 或显式注册一个
Route::options(),返回空响应 + 正确 CORS 头(不推荐,维护成本高)
例如:
Route::any('/api/download', [App\Controller\ExportController::class, 'download']);
而不是:
Route::post('/api/download', [App\Controller\ExportController::class, 'download']);
文件下载场景下 Content-Disposition 的典型写法
前端需要靠 Content-Disposition 提取文件名,后端不仅要设 header,还要确保它被暴露:
控制器中:
public function download()
{
$filename = 'report_' . date('Ymd') . '.xlsx';
$response = response(file_get_contents($path), 200)
->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->withHeader('Content-Disposition', "attachment; filename=\"{$filename}\"");
return $response;
}
注意:
-
filename=后面用双引号包裹,避免中文或空格出错 - 如果文件名含中文,建议用
rawurlencode()编码,并加filename*=UTF-8''{encoded}格式(但需同时暴露该字段) - 务必确认
expose_headers数组里有content-disposition,否则前端response.headers.get('content-disposition')永远是null
最容易被忽略的是:Webman 的 CORS 中间件不会自动继承你在控制器里 set 的 header 到预检响应中——它只管自己配置的那几项。所以 expose_headers 必须提前声明,不能“等用到再加”。

















