curl works, fetch fails: diagnosing CORS and preflight
区分响应访问权限与请求发送能力,检查 OPTIONS 预检请求,并为带凭据的浏览器请求配置明确的源。
本文内容
简明答案
curl 不强制执行浏览器 CORS 规则。成功的 curl 响应不能证明另一个源的 JavaScript 可以读取它。在浏览器 Network 面板中检查:有些请求会直接发送,而 JSON POST 或带有 Authorization 的请求通常需要 OPTIONS 预检。分别检查预检和实际响应,使用页面的确切源以及预期的方法和请求头。
源是方案、主机和端口
位于 https://app.example 的页面与位于 https://api.example 的 API 具有不同的源,即使它们的名称共享后缀。更改端口或从 HTTP 切换到 HTTPS 也可能改变源。首先使用浏览器的实际 Origin 头,而不是根据部署名称猜测。
CORS 决定浏览器是否向脚本暴露跨源响应。它不是 API 身份验证,也不会阻止 curl 或另一个服务器调用 API。将授权和防止不必要的状态更改请求的保护保留在应用程序中。
直接请求和预检是不同路径
没有非安全列表请求头的 GET 通常可以不经过预检发送。某些使用表单兼容内容类型的 POST 请求也可以采用此路径。响应仍需要适当的 CORS 头,JavaScript 才能读取它。
使用 application/json、Authorization 或 PUT 等方法的请求通常需要预检。浏览器会先询问允许哪些方法和请求头,然后再发送应用请求。仅凭凭据并不意味着每个请求都必须预检;检查完整的请求条件。
具体 JSON POST 示例
假设位于 https://app.example 的页面向 https://api.example/items 发送带有 bearer 令牌的 JSON POST。这些是示例主机和假设端点,不是可工作的服务。下面的代码属于浏览器页面;令牌是占位符。
在没有有效缓存的预检结果的情况下,预期会发送一个 OPTIONS 请求,宣传 POST 以及授权和内容类型头。浏览器构造这些预检头;应用 JavaScript 不应手动设置它们。
fetch('https://api.example/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer DEMO_TOKEN'
},
body: JSON.stringify({name: 'sample'})
}).then(response => {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}).then(console.log).catch(console.error);在更改服务器设置之前读取预检
在开发者工具中保留网络日志并定位 OPTIONS。其 Origin 应标识页面,Access-Control-Request-Method 在此示例中应为 POST。Access-Control-Request-Headers 列出请求的非安全列表头。头名大小写不重要。
合适的成功预检响应允许特定源、方法和头。OPTIONS 不应要求后续 POST 所需的 bearer 令牌,因为预检不携带该应用头。身份验证仍需在实际请求中进行。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: authorization, content-type
Vary: Origin实际响应需要自己的权限
通过预检只是第一步。POST 响应还需要 Access-Control-Allow-Origin。检查错误以及成功响应:没有适当 CORS 头的身份验证失败对 JavaScript 来说可能看起来像通用网络失败。
如果 OPTIONS 成功但 POST 失败,检查其状态、应用日志和响应头。CORS 消息不能证明身份验证或应用逻辑成功。相反,直接发送的请求可能已经到达服务器,但浏览器阻止了脚本访问其响应。
Cookie 需要显式源处理
使用 fetch 发送跨源 Cookie 时,使用 credentials: include。服务器必须允许凭据并使用显式允许的源响应,而不是 Access-Control-Allow-Origin: *。浏览器 Cookie 规则,包括 SameSite 和第三方限制,仍然适用。
反射传入源时维护一个允许列表;盲目回显每个源会过度授予访问权限。当响应根据 Origin 变化时,Vary: Origin 有助于缓存分离这些变体。精确匹配源;路径不是源的一部分。
将浏览器请求与命令行进行比较
命令行调用可帮助检查 HTTP 状态和返回的头,但不能重现浏览器的强制执行。比较方法、Origin、请求头、重定向和凭据处理。成功的普通 GET 不能诊断失败的已认证 JSON POST。
不要使用 mode: no-cors 来获取可读的 API 数据。它可能产生不透明响应,脚本无法访问其正文和状态。修复服务器对预期请求的权限,而不是将不透明结果视为成功集成。
简短诊断顺序
首先确认页面和 API 源。然后区分直接请求与 OPTIONS 后跟实际方法。检查预检上的允许源、方法和头;检查应用响应上的源权限。最后检查身份验证和 Cookie 策略。
预检缓存可能抑制之前出现的 OPTIONS 请求。不要仅仅因为没有新的 OPTIONS 条目就推断请求变得简单。此序列将故障缩小到特定交换,而不会削弱 API 访问控制。
检查清单
- 记录浏览器的确切 Origin、方法和请求头。
- 分别检查 OPTIONS 和实际响应,包括错误。
- 对带凭据的响应使用显式允许源。
- 将身份验证和请求伪造保护与 CORS 分开。
适用范围
本指南涵盖常见 fetch 请求。重定向、Cookie 策略、网络故障和应用身份验证可能产生额外故障;CORS 头不能解决所有问题。