FrankenPHP的CORS配置必须在Caddyfile中显式设置,PHP的header()无效因其响应经Caddy中间件处理而被覆盖;需为/api/和/路径分别配置header指令,前置respond处理OPTIONS请求,并避免与credentials=true共存。

FrankenPHP 的 CORS 配置必须写在 Caddyfile,PHP 的 header() 无效
因为 FrankenPHP 底层是 Caddy,所有响应都经过 Caddy 中间件处理,PHP 脚本里调用 header('Access-Control-Allow-Origin: *') 会被覆盖或拦截。你看到的跨域失败,90% 是 PHP 层面加了头但 Caddy 没放行。
必须在项目根目录的 Caddyfile 中为对应路径显式配置 header 指令,且要区分接口路径(如 /api/*)和根路径(如 /):
-
/api/*块需包含Access-Control-Allow-Methods和Access-Control-Allow-Headers,否则浏览器预检(OPTIONS)会失败 - 如果前端直接访问
/index.php或 SPA 的/,也要单独配header / { ... },否则静态资源或首页加载时 CORS 就报错 - 不能写
header * { ... }—— Caddy 会直接报错:“wildcard not allowed in header directive”
OPTIONS 请求必须被 Caddy 显式响应,不能交给 php_server
FrankenPHP 的 php_server 插件默认不处理 OPTIONS 方法,导致预检请求 404 或 500。这不是 CORS 头没加对,而是请求根本没进 PHP 层。
解决方式是在 Caddyfile 中提前匹配并返回 204:
立即学习“PHP免费学习笔记(深入)”;
localhost:8080 {
@options method OPTIONS
respond @options 204 {
header Access-Control-Allow-Origin "*"
header Access-Control-Allow-Methods "GET,POST,OPTIONS,PUT,DELETE"
header Access-Control-Allow-Headers "Content-Type,Authorization,X-Requested-With"
}
php_server
}注意:这个 respond 块必须放在 php_server 之前,否则不生效。
Access-Control-Allow-Origin: * 和 credentials=true 不能共存
如果你前端用了 fetch(..., { credentials: 'include' }) 或设置了 withCredentials = true,后端就不能用通配符 *,否则浏览器直接拒绝响应。
此时必须把 Access-Control-Allow-Origin 改成具体域名,例如:
Access-Control-Allow-Origin "https://myapp.com"- 同时要加
Access-Control-Allow-Credentials "true" - 注意:Caddy 的
header指令不支持变量或条件判断,所以多域名场景得靠反向代理或额外中间件处理
Mercure 实时推送本身也受跨域控制,需单独配
如果你用 Mercure 做实时通知(比如 EventSource('/.well-known/mercure?topic=...')),它的跨域规则和普通 API 是分开的。Caddy 的 mercure 模块默认只允许 HTTPS 订阅,开发时若用 localhost 必须加 insecure:
mercure {
publisher_jwt !ChangeThisMercureHubJWTSecretKey!
subscriber_jwt !ChangeThisMercureHubJWTSecretKey!
anonymous
insecure
}否则即使 CORS 头全配对了,浏览器也会因协议不匹配拒绝连接。另外,Mercure 的 anonymous 开启后仍需确保 Access-Control-Allow-Origin 已覆盖 /.well-known/mercure 路径 —— 它不属于 /api/*,容易漏配。



















