ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Cloudflare Workers CORS问题排查与解决方案

Cloudflare Workers CORS问题排查与解决方案 1. 项目概述CORS 问题到底卡在哪1.1 一次真实的联调事故先说个我自己经历过的场景。上个月给一个前端团队做接口联调前端跑在 localhost:5173后端服务挂在 Cloudflare Workers 上域名是 xxx.workers.dev。前端项目里用 fetch 调接口结果浏览器控制台刷出一整片红色报错Access to fetch at https://xxx.workers.dev/api/user from origin http://localhost:5173 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.前端第一反应是后端代码写错了后端第一反应是前端不该用 fetch 而该用 axios两边来回踢了半小时皮球。最后查下来问题出在一个非常容易被忽略的地方Cloudflare Workers 默认不会在响应里自动附加 CORS 头。也就是说只要你在 Worker 里没有手动设置Access-Control-Allow-Origin任何跨域请求都会被浏览器拦截。这个行为跟传统的 Node.js 后端比如 Express 里配好了 cors 中间件完全不同很多人第一次迁到 Workers 上就直接踩坑。1.2 CORS 是什么为什么总跟 Cloudflare Workers 过不去CORS 全称是 Cross-Origin Resource Sharing中文叫跨域资源共享。它的核心逻辑很简单浏览器发起跨域请求时会先看看目标服务器的响应头里有没有允许当前源Origin访问的声明。如果没有浏览器就直接拦截响应哪怕服务器已经把数据返回来了。Cloudflare Workers 之所以容易出 CORS 问题因为它是一个运行在边缘节点上的 JavaScript 运行时你可以把它理解成一个在全球各地部署的微型服务端。它不像 Express 那样自带中间件生态也没有 Django/Flask 那种框架级的 CORS 处理机制。你是通过fetch事件直接接管请求、自己构造响应的所以响应头完全由你说了算——但也意味着如果忘了加就没有人帮你加。更麻烦的是Workers 通常还承担着反向代理的角色。很多人的架构是这样的浏览器 → Cloudflare Worker → 后端服务比如 FastAPI 或任意 HTTP API。这时候 CORS 问题就变成了两层一是 Worker 返回给浏览器的响应头二是 Worker 转发请求时后端返回的响应头。两层任何一层出错最终都会表现为浏览器里的 CORS 报错。1.3 这个问题适合谁来读这篇文章主要写给下面这几类人把后端服务部署在 Cloudflare Workers 上前端在本地开发或部署在另一个域名上遭遇了跨域拦截。用 Cloudflare Workers 作为 API 网关或反向代理转发请求给 FastAPI / Flask / 其他后端服务但纠结 CORS 头应该配在哪一端。看到has been blocked by CORS policy这类报错就想直接上网搜“关闭跨域”但没搜到有效方案的初学者。如果你属于以上任意一类这篇文章会从排查思路讲起再给出一套可以直接落到 Workers 代码里的解决方案最后补充一些生产环境里才会遇到的细节问题。整个过程不绕弯子都是我实际摸过的路径。2. CORS 机制拆解浏览器到底在拦什么2.1 Origin、请求头与响应头的关系要理解 CORS得先搞清楚三个概念请求方 Origin、目标服务器的响应头、浏览器的拦截逻辑。浏览器在发起跨域请求时会自动带上Origin请求头标明当前页面的来源格式是协议 域名 端口。好比你去银行办业务进门先报上你是从哪个支行来的。服务器收到请求后如果允许这个来源访问就要在响应头里带上Access-Control-Allow-Origin值可以是具体的 Origin也可以是*通配符。浏览器拿到响应后会先检查响应头里有没有允许自己这个 Origin 的声明有则放行没有则拦截。注意一个关键点拦截不是发生在网络层而是发生在浏览器解析层。也就是说服务器其实已经返回了数据请求也确实发出了只不过浏览器不让 JavaScript 读取到响应内容。所以你会发现在 DevTools 的 Network 面板里请求状态甚至可能是 200但控制台照样报 CORS 错误。这个现象是排查时最容易迷惑人的地方。2.2 简单请求与预检请求OPTIONSCORS 把跨域请求分为两类简单请求和非简单请求。简单请求指满足以下条件的请求方法只能是 GET、POST 或 HEADContent-Type 只能是application/x-www-form-urlencoded、multipart/form-data或text/plain且没有自定义请求头。如果请求不满足这些条件——比如用了application/json的 Content-Type或者带上了自定义的Authorization头——浏览器会先发送一个 OPTIONS 请求这叫预检请求Preflight。预检请求的目的是问服务器“我接下来要发一个带 JSON 的 POST 请求还带 Authorization 头你允许吗”服务器需要用Access-Control-Allow-Methods和Access-Control-Allow-Headers来回应告诉浏览器“允许哪些方法和哪些头”。只有预检通过浏览器才会发送真正的业务请求。这就是为什么很多人的 Workers 代码处理了 GET 和 POST却忽略了 OPTIONS 请求——结果预检请求直接被 Worker 当成普通请求路由了返回的是一堆业务数据而不是 CORS 响应头浏览器自然就拦截了。2.3 Cloudflare Workers 的特殊身份既是服务端又是代理Cloudflare Workers 和其他后端服务有一个本质区别它既可以直接响应请求自己写业务逻辑、返回 JSON也可以作为代理把请求转发给上游服务器比如你的 FastAPI 后端再把上游的响应原样返回给浏览器。作为服务端时你需要自己给响应加 CORS 头作为代理时你不仅要保证自己返回的响应带 CORS 头还要注意上游返回的响应头是否会被透传、以及是否会被浏览器接受。我在实际排查中遇到过一种情况Workers 端的 CORS 头配置正确了但上游 FastAPI 也配置了 CORS两个头叠加在一起浏览器反而懵了——这是后话放到第 5 章细讲。3. 排查思路与实操方法3.1 先看报错再动手三类典型报错我在排查 CORS 问题时一般先把报错分门别类因为不同类型的报错对应的问题源头完全不同。第一类是开头提到的No Access-Control-Allow-Origin header is present。这个报错说明响应里压根没有任何 CORS 相关的头。通常发生在 Workers 返回响应时完全没有设置 CORS 头或者 OPTIONS 预检请求没有被正确处理。第二类是The Access-Control-Allow-Origin header contains multiple values *, http://localhost:5173。这种报错一般是多层服务都加了 CORS 头导致同一个响应头出现了两次。请求从 Worker 转发到 FastAPIFastAPI 返回了Access-Control-Allow-Origin: *Worker 又自己加了一个具体 Origin响应头和在一起就变成了两个值。浏览器要求这个头只能有一个值所以直接拒绝。第三类是The value of the Access-Control-Allow-Credentials header in the response is true which must be true when the requests credentials mode is include——听着很绕简单说就是当请求带着 Cookie 或者凭证信息时服务器的Access-Control-Allow-Credentials必须设置为true而且Access-Control-Allow-Origin不能是*必须是具体的 Origin。很多人一上来就把Allow-Origin设成*然后又开了Allow-Credentials: true这俩一组合浏览器直接报错。3.2 用浏览器 DevTools 精准定位请求阶段报错信息只能告诉你结果不能告诉你原因。要定位是哪个环节出了问题第一步是打开 DevTools 的 Network 面板刷新页面触发跨域请求。先看请求列表里有没有 OPTIONS 请求。如果有点开它查看响应头的Access-Control-Allow-*字段。如果 OPTIONS 请求的响应里没有这些头说明是预检请求没有被正确处理。如果没有 OPTIONS 请求说明这是一个简单请求直接看 GET/POST 请求的响应头即可。第二步是看实际的响应头。在请求详情里找到 Response Headers 区域确认access-control-allow-origin这个字段是否存在。如果存在看下值是否包含你的前端 Origin注意端口也算在内http://localhost:5173和http://localhost:4173是不同的 Origin。第三步是看请求头。在 Request Headers 区域找到Origin字段确认浏览器实际发送的 Origin 是什么。有些情况下前端代码里显式设置了mode: no-cors导致浏览器发送的是不透明请求opaque request这种请求下就算服务器返回了 CORS 头你也没法读取——这属于前端代码的问题。3.3 用 curl 直接复现绕开浏览器的判断浏览器拦截是 CORS 错误的第一道防线但也正是因为它“管得太多”导致你没法直接判断是服务器的问题还是浏览器的问题。这时候用 curl 验证是最高效的方式。curl -i https://xxx.workers.dev/api/user \ -H Origin: http://localhost:5173重点看响应头里是否包含access-control-allow-origin: http://localhost:5173。curl 不会像浏览器那样拦截响应它会把所有响应头原封不动打印出来所以你一眼就能看出服务器到底有没有吐出 CORS 头。如果想模拟预检请求可以这样curl -i -X OPTIONS https://xxx.workers.dev/api/user \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: Content-Type, Authorization如果这个 OPTIONS 请求的响应里没有access-control-allow-methods和access-control-allow-headers那问题就锁定了——预检没过。我用这个方法排查过很多次每次都能在 5 分钟内判断出问题是在 Workers 代码还是在上游服务。尽量不要在 DevTools 里反复刷新猜测那样效率太低。4. Workers 端解决方案4.1 最简方案手动构造响应头如果你的 Worker 只是处理简单的 API 请求直接返回 JSON 数据那么最直接的方式就是在fetch事件里给 Response 对象手动添加 CORS 头。export default { async fetch(request, env, ctx) { const response await handleRequest(request); // 给响应统一添加 CORS 头 const corsHeaders { Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, }; const newResponse new Response(response.body, response); for (const [key, value] of Object.entries(corsHeaders)) { newResponse.headers.set(key, value); } return newResponse; }, };这段代码的核心逻辑是先拿到业务逻辑生成的 Response 对象然后new Response(response.body, response)复制一份再往复制后的对象上设置 CORS 头。为什么要复制而不是直接修改原对象因为有些情况下你拿到的响应可能是由fetch()从上游服务器拿到的直接修改它的 headers 会报错。复制一份再修改就安全得多。需要注意的是Access-Control-Allow-Origin: *只适用于不需要携带 Cookie 的场景。如果前端请求是credentials: include模式*会被浏览器拒绝必须改成具体的 Origin这个细节我在 4.3 节讲。4.2 处理 OPTIONS 预检请求关键很多人在 Workers 里写了业务逻辑却忘了处理 OPTIONS 请求导致预检直接打到业务代码里。如果业务代码里面对 OPTIONS 请求返回了 405 Method Not Allowed或者返回了一个不带 CORS 头的 JSON那前端就永远过不了预检。正确的做法是在处理请求一开始就判断方法是否为 OPTIONS如果是直接返回一个 204 响应并带上 CORS 预检所需的头export default { async fetch(request, env, ctx) { // 处理预检请求 if (request.method OPTIONS) { return new Response(null, { status: 204, headers: { Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, Access-Control-Max-Age: 86400, }, }); } // 其他请求正常处理 return handleRequest(request); }, };Access-Control-Max-Age: 86400表示预检结果可以缓存 24 小时也就是 24 小时内同样的请求不会再触发预检。这个值建议设置成 86400即 86400 秒一天既不会太长导致服务端配置变更后客户端还在用旧配置也不会太短导致频繁发预检影响性能。回到刚才那段最简方案你会发现一个问题如果同时处理 OPTIONS 和普通请求CORS 头代码会写两遍非常啰嗦。更好的方案是把 CORS 头统一封装成一个函数我放在 4.4 节。4.3 动态反射 Origin 的安全底线别跟 credentials 搭配生产环境下你的前端可能部署在多个域名比如测试环境是https://test.example.com正式环境是https://app.example.com。这时候如果写死Access-Control-Allow-Origin: *那所有网站都能调用你的 API——可能你不在乎但如果你的 API 涉及用户数据这就是一个安全隐患。所以很多人的做法是动态读取请求头里的Origin原样反射到响应头里const origin request.headers.get(Origin) || ; const corsHeaders { Access-Control-Allow-Origin: origin, Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, };这种做法有个很大的坑反射任意 Origin 等同于允许任何网站跨域调用你的 API。恶意网站只要在自己的页面里发一个 fetch 请求浏览器就会自动带上前端页面的 Origin服务器把这个 Origin 反射回去浏览器就放行了。如果你的接口不依赖 Cookie 验证而是用 Token那还好如果依赖 Cookie 或者 Session那恶意网站就可以利用用户已登录的状态发起请求造成 CSRF 攻击。另外一个必须遵守的底线是反射 Origin 时绝对不能同时设置Access-Control-Allow-Credentials: true。因为浏览器规定当Allow-Credentials: true时Allow-Origin不能是*必须是具体值。很多人为了既能用 Cookie 又能允许多个域名就把Allow-Origin设置成反射的动态 Origin同时把Allow-Credentials设成true。这看起来“很灵活”实际上等于对任意来源放行携带凭证的请求——任何一个恶意网站都能以用户的身份调用你的接口。如果确实需要支持带凭证的跨域请求正确的做法是维护一个白名单const allowedOrigins [ https://app.example.com, https://test.example.com, ]; export default { async fetch(request, env, ctx) { const origin request.headers.get(Origin) || ; const corsHeaders { Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, }; if (allowedOrigins.includes(origin)) { corsHeaders[Access-Control-Allow-Origin] origin; corsHeaders[Access-Control-Allow-Credentials] true; } if (request.method OPTIONS) { return new Response(null, { status: 204, headers: corsHeaders }); } const response await handleRequest(request); return new Response(response.body, { ...response, headers: { ...Object.fromEntries(response.headers), ...corsHeaders }, }); }, };白名单校验通过后才把具体的 Origin 反射回去同时允许携带凭证。白名单之外的来源没有Allow-Origin头浏览器会自然拦截。这个方案既支持多域名又不会引入安全隐患是我在生产环境里一直在用的。注意热搜词里提到的“cors 配置错误(反射 origin credentialstrue)”就是上述问题。很多文章示例代码里直接反射 Origin 又开启 credentials这是网上流传最广的误导性写法千万别照着抄。4.4 把 CORS 处理抽成公共函数在 Workers 里一个 Worker 可能同时处理多个路由用路由匹配/api/user、/api/order等。这种情况下如果每个路由处理函数里都手动加 CORS 头代码会非常冗余而且容易漏加。我的做法是把 CORS 处理抽成一个公共函数统一在入口处处理function createCorsHeaders(request, env) { const origin request.headers.get(Origin) || ; const allowedOrigins [https://app.example.com, https://test.example.com]; const cors { Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers: Content-Type, Authorization, Access-Control-Max-Age: 86400, }; if (allowedOrigins.includes(origin)) { cors[Access-Control-Allow-Origin] origin; cors[Access-Control-Allow-Credentials] true; } return cors; } function handleOptions(request, env) { const cors createCorsHeaders(request, env); return new Response(null, { status: 204, headers: cors }); } async function handleRequest(request, env) { const url new URL(request.url); const cors createCorsHeaders(request, env); if (request.method OPTIONS) { return handleOptions(request, env); } if (url.pathname.startsWith(/api/user)) { const data { name: test, id: 123 }; return new Response(JSON.stringify(data), { headers: { Content-Type: application/json, ...cors }, }); } return new Response(Not Found, { status: 404 }); } export default { async fetch(request, env, ctx) { return handleRequest(request, env); }, };这样设计的好处有几个第一CORS 逻辑集中在一处改白名单只改一个函数第二每个业务响应只需要在构造 Response 时展开cors对象即可不会遗漏第三OPTIONS 预检和普通请求共享同一套 CORS 头保证行为一致。如果你用 TypeScript 写 Workers建议把createCorsHeaders的返回类型定义成Recordstring, string这样在展开到 Response headers 时不会出现类型报错。5. 与 FastAPI 后端配合时的双层 CORS5.1 Worker 代理 FastAPI 时的常见坑很多人的架构不是把整个后端逻辑写在 Workers 里而是用 Workers 作为反向代理转发请求到阿里云或者本地的 FastAPI 服务上。这时候就出现了双层 CORS浏览器 → Worker第一层响应头Worker → FastAPI第二层响应头。第一层是浏览器能直接看到的响应头由 Worker 决定。第二层是 Worker 转发请求后FastAPI 返回的响应头Worker 可以选择透传也可以选择覆盖。最常见的坑是FastAPI 已经配置了 CORSMiddleware会自动给响应添加Access-Control-Allow-Origin。而 Worker 在做代理转发时直接把上游响应原样返回给浏览器没有覆盖或删除 FastAPI 加的 CORS 头。如果你同时又在 Worker 里自己加了Access-Control-Allow-Origin那响应里就会出现两个Access-Control-Allow-Origin头浏览器直接报multiple values错误。我遇到过的最迷惑案例是FastAPI 返回Access-Control-Allow-Origin: *Worker 返回Access-Control-Allow-Origin: https://app.example.com两个头前后拼接浏览器认为响应非法直接拦截。表面上看每个服务都配置得很正确合在一起却变成了错误。5.2 FastAPI 端 CORSMiddleware 的正确配置如果你用的是 FastAPI通常会这样配置from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[https://app.example.com], allow_credentialsTrue, allow_methods[*], allow_headers[*], )这个配置本身没问题但要注意如果前面还有 Cloudflare Worker 挡着FastAPI 其实只需要接受来自 Worker 的请求而 Worker 的请求是没有 Origin 头的或者说是由 Worker 自身发起的。所以FastAPI 在大多数情况下根本不需要配置 CORS因为 Worker 才是它的客户端真正需要 CORS 的是 Worker 和浏览器之间的通信。如果 Worker 转发请求时把浏览器的Origin头原样传递给了 FastAPI那么 FastAPI 的 CORSMiddleware 也会生效就会产生双层 CORS 头叠加的问题。解决办法有二第一种在 Worker 转发请求时把Origin头删掉让 FastAPI 认为这是一个同源请求第二种在 Worker 拿到 FastAPI 的响应后删除其中所有Access-Control-*头再添加 Worker 自己的 CORS 头。我建议用第一种因为更干净——FastAPI 根本无需感知 CORS 的存在。5.3 双层 CORS 头叠加问题与解决给大家一个我实际使用过的转发方案里面对 CORS 头做了完整的清理和重建async function proxyRequest(request, env) { const url new URL(request.url); const targetUrl env.UPSTREAM_BASE_URL url.pathname url.search; const forwardHeaders new Headers(request.headers); // 清理可能干扰 CORS 的请求头 forwardHeaders.delete(Origin); forwardHeaders.delete(Host); const upstreamResponse await fetch(targetUrl, { method: request.method, headers: forwardHeaders, body: request.method GET || request.method HEAD ? undefined : request.body, redirect: manual, }); // 创建新响应剥离上游的 CORS 头 const responseHeaders new Headers(upstreamResponse.headers); responseHeaders.delete(Access-Control-Allow-Origin); responseHeaders.delete(Access-Control-Allow-Methods); responseHeaders.delete(Access-Control-Allow-Headers); responseHeaders.delete(Access-Control-Allow-Credentials); // 重新添加 Worker 这一层的 CORS 头 const cors createCorsHeaders(request, env); for (const [key, value] of Object.entries(cors)) { responseHeaders.set(key, value); } return new Response(upstreamResponse.body, { status: upstreamResponse.status, statusText: upstreamResponse.statusText, headers: responseHeaders, }); }这里有两个细节值得注意。第一forwardHeaders.delete(Origin)能避免 FastAPI 的 CORSMiddleware 生效从根源上防止双层 CORS。第二把上游响应里的Access-Control-*头全部删掉再重建能确保最终返回给浏览器的 CORS 头是唯一的、可控的。如果你没法改 Worker 代码比如用的是一个现成的网关那就只能在 FastAPI 端想办法了。可以让 CORSMiddleware 的allow_origins设置为一个不会匹配到任何请求的值比如[https://worker.invalid]这样 CORSMiddleware 虽然存在但不会实际生效。不过这种方式比较 hack能改 Worker 还是优先改 Worker。6. 常见问题与排查技巧实录6.1 常见问题速查表我在实际排查中积累了一些高频问题整理成一个速查表方便各位对照排查。现象可能原因排查方法解决方案报错 NoAccess-Control-Allow-OriginheaderWorker 响应没加 CORS 头curl 加-H Origin: ...看响应头在 Worker 入口统一添加 CORS 头OPTIONS 请求返回 404/405Worker 没处理预检DevTools Network 看 OPTIONS 请求状态在代码中拦截 OPTIONS 返回 204响应头出现多个 Allow-Origin 值Worker 和上游都加了 CORS 头curl 查看完整响应头Worker 转发时删除上游 CORS 头携带凭据的请求被拦截Allow-Origin: *Allow-Credentials: true检查前端是否用credentials: include改为白名单动态 Origin credentials修改 Worker 后浏览器还是报错浏览器缓存了预检结果硬刷新或等待 Max-Age 过期临时把 Max-Age 设小或者用隐私窗口测试Worker 转发后响应头丢失上游响应头处理逻辑有问题在 Worker 里 log 上游响应头用new Headers(upstreamResponse.headers)复制第 5 行特别值得多说几句。预检结果会被浏览器缓存Access-Control-Max-Age设得越大缓存时间越长。如果你改了 Worker 里的 CORS 配置但浏览器还在用旧的缓存预检结果就会造成“明明改了为什么还报错”的假象。排查时优先用无痕窗口可以避免缓存干扰。6.2 我踩过的一些坑和最终心得排查 CORS 问题本质上是在理清一条链路浏览器 → Worker → 上游服务器。只要链路上每一个环节的响应头都在你的掌控之中问题就不会存在。所以我最后的建议就两条第一CORS 头必须在最外层统一处理不要散落在各个业务代码里第二转发代理时要主动清理上游的 CORS 头保证响应头唯一。我最初做 Workers 开发时也走过不少弯路当时甚至想过用mode: no-cors绕过跨域结果发现响应变成不透明请求前端根本拿不到数据纯属南辕北辙。还有一次为了省事把所有 CORS 头都设成*后来上线后才发现接口可以被任何网站调用赶紧加白名单才堵上。希望读到这里的你不要再踩这几个坑。最后分享一个小技巧在开发阶段可以在 Worker 的代码里临时加一个只对开发环境生效的宽泛 CORS 规则Allow-Origin: *等联调完成后再切到白名单模式。这样既不影响开发效率又能保证生产环境的安全。切换的时候记得用无痕窗口验证避免预检缓存干扰测试结果。
返回列表