ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

DELETE请求总报跨域?别忽略Access-Control-Allow-Methods

DELETE请求总报跨域?别忽略Access-Control-Allow-Methods 做前后端联调时跨域问题就像空气里随时会爆的小雷。最常见的一种形态是接口上线后 GET、POST 都正常唯独 DELETE 在浏览器控制台疯狂报跨域CORS错误排了半天发现是后端配置里 Access-Control-Allow-Methods 没把 DELETE 放进去。这篇文章就是专门拆解这个现象为什么 DELETE 比 GET、POST 更容易踩跨域坑以及不同技术栈下到底要怎么修、怎么排查。很多第一次遇到这个问题的同学会以为是前端代码把方法写错了或者在浏览器上做了什么奇怪设置。其实只要跨域配置里漏了 DELETE浏览器就会用一套完全不同的拦截逻辑来对待它而 GET、POST 不需要经过那么严格的检查所以你才会看到“其他 method 都没事”的错觉。1. 先想清楚为什么 GET/POST 可以不报错DELETE 却一定触发跨域拦截1.1 “简单请求”和“预检请求”的分水岭浏览器的跨域拦截不是对所有请求一视同仁的。根据 CORS 规范请求会被分成两类简单请求和预检请求。简单请求的判定条件很严格基本要求是方法只能是 GET、HEAD、POST 三种之一没有设置自定义请求头比如常见的 AuthorizationContent-Type 只能是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain 中的一种。只要满足这些条件浏览器会直接发送正式请求同时在响应头里检查 Access-Control-Allow-Origin和当前页面的域名是否匹配检查通过就算完。很多项目里 GET 请求默认不会带复杂请求头所以后端只要配了Access-Control-Allow-Origin: *GET 请求就能正常跑通。POST 也经常被归类为简单请求但前提是没带自定义头并且 Content-Type 是表单格式。很多前端用原生 XHR 或 axios 发 POST 时如果代码没有额外设置Content-Type: application/json实际上会按表单格式上传那 POST 就真的成了简单请求跨域检查非常宽松。DELETE 则完全不同。这个 HTTP 方法天然不在简单请求名单里它不可能成为简单请求。不管你的请求头多干净、Content-Type 多普通DELETE 都会被浏览器强制列入预检请求的队列。这一点是 DELETE 跨域问题出现的根源。1.2 预检请求到底在“对答案”什么预检请求并不是直接发送 DELETE而是先由浏览器自动发起一个 OPTIONS 请求。这个 OPTIONS 请求看起来很奇怪路径和真实 DELETE 一样但请求头里会额外带上Access-Control-Request-Method: DELETE以及实际请求中会出现的关键请求头比如Access-Control-Request-Headers: content-type, authorization浏览器发这个 OPTIONS 的目的就是先去询问服务器“我待会要从 http://localhost:3000 这个来源发一个 DELETE 请求路径是当前这个你允许吗”服务器需要在这个 OPTIONS 响应中给出至少三样东西Access-Control-Allow-Origin允许的来源Access-Control-Allow-Methods允许的请求方法列表Access-Control-Allow-Headers允许的自定义请求头列表如果有的话。如果这三样里有任何一样不满足浏览器就会在控制台报错并且不会发送真正的 DELETE 请求。我见过很多后端同事查日志说“我明明没有看到 DELETE 请求进来”这是因为请求根本没发送到后端全被浏览器挡住了。理解了预检机制你就明白了“GET 能通、POST 能通、DELETE 不能通”并不是玄学而是请求在浏览器侧做的安全检查不一样。接下来真正要排的就是服务器返回的 Allow-Methods 头里到底有没有 DELETE。2. 重点排查 Access-Control-Allow-Methods配置是怎么悄悄漏掉 DELETE 的2.1 Allow-Methods 不是“自动反射”很多框架的 CORS 插件并不会自动把你接口支持的所有方法放到 Access-Control-Allow-Methods 里而是让你写一个白名单。你写什么预检响应里就返回什么。这个配置是静态的不是你接口路由里有 DELETE浏览器就会自动认为允许 DELETE。所以最常见的翻车现场是这个样子的Access-Control-Allow-Methods: GET, POST, PUT后端开发者刚开始可能只想暴露查询和新增接口就写了 GET 和 POST后来接口慢慢加上了 PUT、DELETE但 CORS 配置没有同步更新。结果就是GET、POST、PUT 都能正常请求DELETE 一上来就被预检拦掉。浏览器控制台会显示大致这样的一句话Access to XMLHttpRequest at https://api.example.com/user/123 from origin http://localhost:3000 has been blocked by CORS policy: Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.这里的关键词是Method DELETE is not allowed。如果你看到的是这个提示那基本可以确定Allow-Methods 列表里没有 DELETE。解决办法也很简单在后面加一个DELETE就行。2.2 配置不是只有一层框架、网关、Nginx 都在发“证书”在实际生产环境里CORS 响应头不一定都是应用代码发出的。有些团队会把跨域配置统一放在 API 网关有些会在 Nginx 上统一加头有些会放在框架中间件里处理。问题就出在链路中任意一层返回的 Allow-Methods 头不包含 DELETE都会被浏览器揪出来。我用一个例子说明。假设前端访问路径是https://www.example.com/api/userNginx 负责转发到后端服务同时 Nginx 也在 location 块里配置了 CORS 头location /api/ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, OPTIONS; }这个配置里漏掉了 DELETE。结果就是无论后端 FastAPI 或 Express 里怎么允许 DELETE浏览器最终看到的合法响应头还是 Nginx 给的GET, POST, OPTIONS预检依旧失败。还有一种情况Nginx 的 add_header 指令只在某个 location 中存在另一个接口路径没有继承到后端返回的头又没有带全DELETE 自然就崩了。排查的时候不要只盯着应用代码看要沿着浏览器收到的响应去追看这些响应头到底是从哪一层加上去的。检查方法也很简单打开浏览器的开发者工具切到 Network 面板找到 OPTIONS 请求点开 Response Headers直接看你看到的 Allow-Methods 是什么。如果和预期不符就再去 Nginx、网关层搜索一下看有没有别的地方覆盖了响应头。3. 不同技术栈的 DELETE 跨域修复现场3.1 Express / Node.js中间件里显式允许 DELETE在 Node.js 的 Express 项目里如果没用现成的 cors 包自己写中间件是最容易出问题的。我推荐直接使用官方cors包然后这样配置const cors require(cors); app.use(cors({ origin: http://localhost:3000, methods: [GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization], }));如果你习惯自己写中间件也一定要把 OPTIONS 请求短路掉避免让 OPTIONS 落到后面的业务路由被错误当成普通请求处理app.use((req, res, next) { res.header(Access-Control-Allow-Origin, req.headers.origin || *); res.header(Access-Control-Allow-Methods, GET, POST, PUT, PATCH, DELETE, OPTIONS); res.header(Access-Control-Allow-Headers, Content-Type, Authorization); if (req.method OPTIONS) { return res.status(204).end(); } next(); });很多人写到这里会忽略 OPTIONS 的处理结果就是GET、POST 都通DELETE 预检请求发送到 Express 后因为没有对应的路由返回 404浏览器照样报错。但错误信息可能不是Method DELETE is not allowed而是Response to preflight request doesnt pass access control check: It does not have HTTP ok status.这时候要先检查 OPTIONS 请求是不是被短路了。3.2 PHP 后端header 一次性写给够PHP 后端的 CORS 配置一般直接在入口文件或者公共控制器里写 Header。关键点是OPTIONS 预检请求也要返回同样的 Header不能只在正式请求里返回。header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); header(Access-Control-Max-Age: 86400); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit(); }这里有一个容易漏的细节如果你用 Laravel、ThinkPHP 这类框架而且路由是Route::resource之类的资源路由其实框架已经能处理 DELETE 方法但 OPTIONS 请求可能会被框架的 CSRF 中间件挡住。做前后端分离项目时建议把 OPTIONS 请求单独放进排除清单或者提前在中间件里返回。还要顺便说一个老同学常问的问题为什么不直接上 JSONPJSONP 原理是动态创建 script 标签浏览器只允许它发 GET 请求DELETE 请求根本不会通过 JSONP 发出去。所以如果后端只提供 JSONP 跨域方案那前端面对 DELETE 接口时依然无解只能老老实实把 CORS 的 Allow-Methods 配好。3.3 Nginx 反向代理先挡 OPTIONS再放真实请求如果项目是用 Nginx 做反向代理建议在 location 里把 OPTIONS 请求单独处理掉。因为后端能不能收到 OPTIONS 预检请求取决于上游服务是否支持。与其依赖后端不如直接在 Nginx 给预检请求返回一个完整的 CORS 响应头。location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin $http_origin; add_header Access-Control-Allow-Methods GET, POST, PUT, PATCH, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization; add_header Access-Control-Max-Age 86400; add_header Content-Length 0; add_header Content-Type text/plain; return 204; } add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods GET, POST, PUT, PATCH, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization always; add_header Access-Control-Allow-Credentials true always; proxy_pass http://backend_server; proxy_set_header Host $host; # 其他 proxy 参数 }这里有几个重点。第一个是$http_origin它不是固定值而是读取请求头里的 Origin按当前来源动态返回比写死*更安全尤其当接口需要携带 Cookie 或 Authorization 凭证时不能用通配符。第二个是always参数默认情况下 add_header 只会在 200、201、204、206、301、302、303、304、307、308 这些响应状态码下添加加上 always 以后即使返回 400、500 等错误状态响应头也会带上 CORS 头避免报错时浏览器看不懂错误原因。第三个是 return 204 前要先把必要的响应头都 add 完否则预检响应可能缺少关键头。3.4 FastAPI / PythonCORSMiddleware 的 allow_methods 列表FastAPI 做跨域配置很简单但踩坑往往就踩在 allow_methods 上。很多人会这样写from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsTrue, allow_methods[GET, POST], allow_headers[*], )这个配置里 GET 和 POST 没问题DELETE 就会被预检拦掉。因为 allow_methods 明确只写了 GET 和 POST。FastAPI 不会因为你定义了一个app.delete(/user)就自动把 DELETE 加进去。你需要把 allow_methods 改成allow_methods[GET, POST, PUT, PATCH, DELETE, OPTIONS],或者偷懒写成allow_methods[*],但有一个大坑如果你同时设置了allow_credentialsTrue那么 allow_origins 不能是[*]必须写成明确的来源列表否则浏览器会报The value of the Access-Control-Allow-Origin header in the response must not be the wildcard *。所以在需要携带 Cookie 或 Authorization 的 DELETE 请求中我建议要么去掉allow_credentialsTrue要么把来源写清楚。4. 当后端方法列表已经正确DELETE 还是报错还有哪些隐蔽因素4.1 OPTIONS 请求被网关、WAF、云防护拦下还有一种比较隐蔽的情况后端和 Nginx 都配置了 DELETE浏览器里看预检响应也是正确的但 DELETE 请求依然报跨域错误。这时候可以检查一下 OPTIONS 预检请求本身是否被安全设备拦截了。很多生产环境会部署 Web 应用防火墙WAF或云防护策略这些策略默认只允许 GET、POST、HEAD 方法。对于 OPTIONS、DELETE、PUT 这类方法一些严格的安全规则会直接返回 403 或 405。浏览器看到 OPTIONS 预检请求得到一个非 2xx 的状态码就会直接判定跨域失败。排查方法是在命令行里手动模拟预检请求curl -i -X OPTIONS https://api.example.com/user/123 \ -H Origin: http://localhost:3000 \ -H Access-Control-Request-Method: DELETE如果返回的不是 2xx而是一段 HTML 错误页或 403 页面那问题基本不在后端应用而在中间链路。需要去调整网关、WAF 或云接入层的方法白名单把 OPTIONS 和 DELETE 放行。4.2 前端自定义请求头把预检复杂度拉高DELETE 请求经常需要带令牌很多前端会在 axios 里统一加上Authorization请求头。于是预检请求发出时不仅带了Access-Control-Request-Method: DELETE还会带Access-Control-Request-Headers: authorization。这时服务器不仅要返回 Allow-Methods 包含 DELETE还要返回 Allow-Headers 包含 authorization。如果 Allow-Headers 只配了Content-Type那浏览器同样会拦截 DELETE因为自定义头不通过。控制台错误信息通常是这样的Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.解决办法就是把 Authorization、X-Requested-With 这类请求头加进 Allow-Headers。如果你不确定前端会带哪些自定义头可以暂时配Access-Control-Allow-Headers: *但要注意和 Allow-Methods 不同带凭证场景下的通配符需要谨慎使用。4.3 浏览器缓存了失败预检还有一个很容易让人崩溃的细节浏览器会缓存预检请求的结果。配置明明改对了浏览器却还在用老旧的失败结果拦截请求。预检缓存的时间和Access-Control-Max-Age响应头有关。如果你之前返回了一个较大的 Max-Age比如 86400 秒而那时候的预检配置有问题浏览器就会把失败结果缓存一整天。你改完后端配置以为立刻生效其实浏览器还在拿一个小时前缓存的错误结果来拦你。遇到这种情况最先要做的不是反复改代码而是在浏览器开发者工具里开启 Disable cache或者强制刷新页面也可以用无痕窗口重新验证。如果无痕窗口下 DELETE 请求正常那就说明是预检缓存问题。4.4 方法名大小写和路由规范带来的假象HTTP 方法名按规范是区分大小写的标准方法一般都用大写。如果后端配置里用了小写delete有些框架可能不会严格按规范处理响应里返回的允许方法列表就变成了delete浏览器按大小写敏感的方式去匹配就会判定不通过。这类问题之所以隐蔽是因为前端代码里你看到的明明是DELETE后端代码里写的也是DELETE但框架底层在拼接响应头时可能做了归一化处理。我的经验是跨域配置里统一使用大写方法名不要写小写也不要在字符串里混入空格。比如GET, POST, PUT, DELETE, OPTIONS这种写法最稳妥别写成GET, POST, PUT, DELETE,OPTIONS或,DELETE这种带奇怪逗号的格式。5. 快速定位 DELETE 跨域问题排查清单与错误对照5.1 从浏览器 Network 面板按顺序做三层判断不要再靠猜了打开开发者工具按下面的顺序一步步看先刷新页面触发 DELETE 请求打开 Network 面板找到那个被浏览器以跨域错误标红的请求。注意看请求类型到底是 OPTIONS 还是 DELETE。如果只有 OPTIONS 请求没有 DELETE 请求说明预检没有通过。点开 OPTIONS 请求查看 Response Headers 里的 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers。如果 OPTIONS 请求状态不是 2xx则是网关、Nginx 或后端路由没处理好 OPTIONS需要解决的是 OPTIONS 的响应问题。如果 OPTIONS 返回 2xx但 DELETE 没有后续发出仔细对比Access-Control-Request-Method和响应里的Access-Control-Allow-Methods再看Access-Control-Request-Headers和Access-Control-Allow-Headers。如果 DELETE 请求已经发出并且后端也正常处理了但响应体拿不到那就是响应头缺少 Access-Control-Allow-Origin 之类的头需要在正常接口路径上也加上跨域头。这里强调一个容易误解的地方很多人觉得跨域配置只需要在 OPTIONS 上做就行其实正式请求的响应头也必须带 Access-Control-Allow-Origin。如果正式 DELETE 返回的头里没有这个字段浏览器一样不会把响应交给前端 JS。5.2 错误信息与解决方案对照速查浏览器报错关键词大概率问题直接对策Method DELETE is not allowed by Access-Control-Allow-Methods预检响应里的 Allow-Methods 没包含 DELETE在框架、Nginx、网关层把 DELETE 加进 Allow-MethodsIt does not have HTTP ok statusOPTIONS 预检请求返回了 4xx 或 5xx检查 OPTIONS 是否被 WAF/网关拦截或后端没有处理 OPTIONSRequest header field authorization is not allowed预检里的 Allow-Headers 没包含 Authorization 等自定义头把 Authorization、X-Requested-With 等加入 Allow-HeadersAccess-Control-Allow-Origin header must not be the wildcard *使用了通配符来源但请求带了凭证改用明确的 Origin或去掉 allow_credentials无痕窗口能通普通窗口不通浏览器缓存了失败预检开启 Disable cache或在服务端下调 Access-Control-Max-Age这张表基本覆盖了我在项目中遇到的 DELETE 跨域问题的所有形态。实际排障时先看报错里的关键词再针对性地改配置通常几分钟就能定位。6. 我的排障心得这个场景我前前后后踩过好多次最有体会的一点是DELETE 跨域问题不是“DELETE 本身有什么特殊魔法”而是它一定触发预检预检响应里任何一个头不对都会被拦。GET 和 POST 因为可能没触发预检绕过了严格检查所以显得“没事”。很多同事把跨域问题想成“后端接口有没有允许跨域”其实准确的说法是“浏览器看到的响应头有没有通过 CORS 规则校验”。最后再分享一个我特别受用的习惯改完 CORS 配置后先别急着刷新页面看结果先用 curl 手动模拟一次 OPTIONS 请求确认响应头完全正确再回到浏览器验证。这样能把后端问题和浏览器缓存问题分开来省掉很多“我以为改好了但还是报错”的无效循环。如果你也要排查 DELETE 跨域按这个方法走至少能把排查时间缩短一半。
返回列表