拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
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 三种之一;
  • 没有设置自定义请求头(比如常见的 Authorization);
  • Content-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/user,Nginx 负责转发到后端服务,同时 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 doesn't 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 请求单独放进排除清单,或者提前在中间件里返回。

还要顺便说一个老同学常问的问题:为什么不直接上 JSONP?JSONP 原理是动态创建 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 / Python:CORSMiddleware 的 allow_methods 列表

FastAPI 做跨域配置很简单,但踩坑往往就踩在 allow_methods 上。很多人会这样写:

from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, 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_credentials=True,那么 allow_origins 不能是["*"],必须写成明确的来源列表,否则浏览器会报The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'。所以在需要携带 Cookie 或 Authorization 的 DELETE 请求中,我建议要么去掉allow_credentials=True,要么把来源写清楚。

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 面板按顺序做三层判断

不要再靠猜了,打开开发者工具,按下面的顺序一步步看:

  1. 先刷新页面,触发 DELETE 请求,打开 Network 面板,找到那个被浏览器以跨域错误标红的请求。注意看请求类型到底是 OPTIONS 还是 DELETE。
  2. 如果只有 OPTIONS 请求,没有 DELETE 请求,说明预检没有通过。点开 OPTIONS 请求,查看 Response Headers 里的 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers。
  3. 如果 OPTIONS 请求状态不是 2xx,则是网关、Nginx 或后端路由没处理好 OPTIONS,需要解决的是 OPTIONS 的响应问题。
  4. 如果 OPTIONS 返回 2xx,但 DELETE 没有后续发出,仔细对比Access-Control-Request-Method和响应里的Access-Control-Allow-Methods,再看Access-Control-Request-Headers和Access-Control-Allow-Headers。
  5. 如果 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-Methods
It does not have HTTP ok statusOPTIONS 预检请求返回了 4xx 或 5xx检查 OPTIONS 是否被 WAF/网关拦截,或后端没有处理 OPTIONS
Request header field authorization is not allowed预检里的 Allow-Headers 没包含 Authorization 等自定义头把 Authorization、X-Requested-With 等加入 Allow-Headers
Access-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 跨域,按这个方法走,至少能把排查时间缩短一半。

返回列表