开始前,会这些就够了
- 理解请求方法、响应状态和 Content-Type。
- 准备 Python 3.10+ 与正常启用安全策略的浏览器。
- 只在自己的本机运行;不需要账号、数据库或外部 API。
页面点击保存后,浏览器报告跨域错误。开发者很容易判断“请求被挡住,所以服务端没有修改”,随后再点一次保存。但跨域错误可能发生在读取响应的阶段:请求已经到达,服务端也已经处理,只是页面脚本没有得到读取响应的许可。
本项目用 Python 标准库开两个仅监听 127.0.0.1 的端口,一个提供页面,一个模拟 API。所有效果和观察记录只存在当前进程内存中。你会亲手区分“POST 已发送但响应不可读”和“预检失败,所以实际 POST 没发出”。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本和现代浏览器;制作时用 Python 3.14.0 与 Codex 内置浏览器实测。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- cors_local.py:完整示例
- README.md:环境、范围与独立练习
- browser-observations.json:制作时的观察记录
- verification.json:对应源码的验证范围
- SHA256SUMS.txt:文件内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B cors_local.py运行后打开终端打印的本机地址,分别点击三个按钮,对照同一编号的浏览器结果、API 方法与模拟写入次数。端口每次自动选择。Ctrl+C 停止两个服务器;记录只在内存中。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
都在本机,也可以不是同一个源
对普通 HTTP 地址,同源比较涉及协议、主机和端口。本例主机与协议相同,但程序为页面和 API 分配两个不同端口,因此跨源。浏览器通常不允许页面脚本直接读取另一个源的响应,需要由响应中的 CORS 头说明是否共享;这个机制叫跨源资源共享。
“页面脚本可以读响应”与“服务端愿意处理这次业务”是不同判断。CORS 不能代替登录、对象权限与业务校验,也不能把来自任意客户端的请求都挡在服务器外。命令行 HTTP 客户端一般不替浏览器执行这套读取限制,因此命令行能拿到正文,并不能证明页面调用也能成功。
启动一次,然后按按钮逐个观察
下载本篇练习包,解压后在 cors_local.py 所在目录运行 python cors_local.py。终端会打印页面地址和 API 地址;端口由系统选择,每次启动可能不同。用浏览器打开打印的页面地址,不要直接把源码另存成文件后用 file 地址打开,否则实验前提已经变化。
每次点击都会生成新的试验编号,页面显示浏览器结果、API 收到的方法和模拟写入次数;终端按同一编号打印记录。页面随后通过自己的同源 /state 接口读取观察记录,避开被研究的跨源响应限制。favicon、页面加载和观察查询不计入 API 写入,避免把无关请求算成结果。
python --version
python cors_local.py
# 在浏览器打开终端打印的页面地址,依次点击三个按钮。
# 实验结束:回到终端按 Ctrl+C。怎样运行:解压本篇练习包,在 cors_local.py 所在目录运行。只用 Python 标准库;需要浏览器实际点击,命令行请求不能替代 CORS 验证。
运行后对照结果
端口号每次可能不同。
按钮 1:响应不可读;方法 [POST];模拟写入 1。
按钮 2:响应不可读;方法 [OPTIONS];模拟写入 0。
按钮 3:可读取 HTTP 200;方法 [OPTIONS, POST];模拟写入 1。
以上是按本实验条件定义的预期;应以你浏览器和同编号服务端记录核对。第一种:实际 POST 到了,正文却读不到
点击“普通 POST:响应不共享”。它用 POST、text/plain 和短文本,没有额外自定义请求头,也不携带凭据。在本例的条件下,浏览器可以直接发送实际请求,无需先做 CORS 预检。API 收到后增加一次模拟写入,并返回 200,却故意不提供允许共享的响应头。
预期页面报告无法读取响应,但同编号记录包含 POST,模拟写入次数为一。这证明的是本机假服务记录过这次效果,不是任何真实订单已经入库。若拿同类页面错误当成“服务端肯定没做事”的证据,自动换一个业务编号重试,就可能制造重复效果;真实系统应另有结果查询和重试约定。
第二种:先问能不能发,答案不合格就不发 POST
点击“JSON POST:预检不放行”。本次仍然是 POST,但显式使用 application/json,这不在本例无需预检的 Content-Type 范围里。浏览器先向目标地址发 OPTIONS,带上准备使用的方法及请求头信息,让服务器表明是否接受这种跨源调用。预检不是业务提交,不应在它里面执行保存。
假服务对 OPTIONS 返回 204,却没有给出 CORS 允许头。状态码看起来成功仍不够:预检需要满足跨源检查。预期同编号仅记录 OPTIONS,模拟写入次数为零,浏览器也报告无法读取。它与第一种错误文案可能相似,但方法记录说明二者停在不同阶段;不能只靠一个 TypeError 判断服务端发生了什么。
第三种:预检和实际响应都明确放行
点击“JSON POST:预检与响应都放行”。这条路径只为本次页面的准确源返回 Access-Control-Allow-Origin,并声明 POST 与 Content-Type 可用于调用。实际 POST 的响应同样带允许共享的头,所以页面能够读出 200 和 stored=true。预期记录先有 OPTIONS,再有 POST,模拟写入一次。
只给 OPTIONS 加头还不够,后面的实际响应也要通过 CORS 检查。项目为每次试验换新编号并关闭预检缓存的保存时长,便于观察;真实浏览器可能复用有效的预检结果,所以没看到新的 OPTIONS,不能独立证明没有走过预检策略。本例主动省略凭据,不讨论 Cookie、第三方 Cookie 政策或带凭据的通配符规则。
把浏览器证据和服务端证据放在一起
验收时为三个按钮各保存一次独立编号,记录脚本可否读取、API 方法序列和效果数。需要解释具体拒绝原因时查看浏览器控制台与网络面板;脚本捕获到的错误通常不包含完整的 CORS 诊断。如果三个按钮全都失败,先检查服务是否仍运行、地址是否来自本次启动,以及本机浏览器策略,不要修改预期来迁就现象。
本篇在 2026 年 10 月 13 日通过 Codex 内置的 Chromium 浏览器实际点击了三条路径,观察到前两条脚本报错、第三条读到正文,以及一、零、一的模拟效果数;同编号方法记录与预期一致。这份记录仅支持该次本机浏览器与假服务的组合,没有覆盖所有浏览器,也没有测试任何真实保存接口。
实验只研究两个本机源之间的 fetch 行为,没有 HTTPS、代理、重定向、身份认证和持久业务。Python 的 http.server 也不是生产服务器模板。结束后回终端按 Ctrl+C,两个端口关闭,内存记录消失。正式接口应独立检查身份与业务权限;CORS 负责浏览器如何共享响应,无法替这些检查收尾。
记住这三点
- 跨源错误可能出现在收到实际响应之后,不能独立证明业务未执行。
- 预检失败与实际响应不共享是两条不同路径,要结合方法记录区分。
- 预检放行和实际响应共享都需要满足对应检查。
别把例子用过头
- 把所有跨域报错都解释成请求未发出,随后盲目重试写入。
- 只看到 OPTIONS 返回 204,就认为预检已成功。
- 用命令行响应或只通过 Python 语法检查,冒充浏览器 CORS 验证。
自己试一小步
如果只允许预检通过,却不给实际 POST 响应共享权限,页面、方法记录和效果数应怎样变化?
- 复制本机实验,新增一个独立按钮与路径;先分别预测浏览器可读性、OPTIONS、POST 和效果计数。
- 只修改新路径的响应头逻辑,保留原三条作为对照;不要关闭浏览器安全策略。
- 再设计一次同编号重复提交的观察,说明试验编号为何只是关联证据,不能自动提供业务幂等。
做到这些,就算完成
- 提交新增路径的预测与实测记录,能用同一编号对应浏览器和假服务。
- 没有把预检请求计作业务写入,也没有因为错误提示而忽略已记录的 POST。
- 没有连接真实账户或外部写接口,结论限定在本机实验。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- WHATWG Fetch:CORS 协议 (新标签页)↗
阅读 3.3.1—3.3.3 的响应共享、预检请求和响应头,以及 4.8 的预检流程;支持两阶段检查,不把 HTTP 204 单独当作通过。
- MDN:CORS 请求场景 (新标签页)↗
阅读 Simple requests、Preflighted requests 与脚本不能取得详细错误原因的说明;支持 text
/plain与 application/json在本例中的不同路径。 - WHATWG HTML:源与同源判断 (新标签页)↗
阅读 7.1.1 中 tuple origin 和 same origin 算法;支持普通 HTTP 地址协议、主机、端口的比较,不把不同端口当成同源。
- Python 3.12:http.server (新标签页)↗
阅读
ThreadingHTTPServer、BaseHTTPRequestHandler的按方法分派与非生产用途提示;支持本地两服务实现及其范围。 - RFC 9110:Content-Length (新标签页)↗
阅读第 8.6 节关于 204 响应不得发送 Content-Length 的要求;本机服务器的 OPTIONS 204 分支不写正文,也不发送该头。