diff --git a/hzh/DEV/跨域问题调试.md b/hzh/DEV/跨域问题调试.md
new file mode 100644
index 0000000..83e7d2e
--- /dev/null
+++ b/hzh/DEV/跨域问题调试.md
@@ -0,0 +1,269 @@
+---
+tags: [前端, 后端, 跨域, 调试, 网络, Cookie, 缓存]
+create time: 2026-04-29 15:30
+---
+
+# 跨域问题调试
+
+## 概述
+
+本文档是面向实际开发中的跨域排查指南,从"控制台一条红色报错"开始,逐步教你定位问题根因。不同于原理型笔记,这里关注的是**你真正会踩的坑**。
+
+## 正文
+
+### 第一步:读懂浏览器的错误信息
+
+浏览器控制台只会报一句话,但这句话已经包含了所有关键信息:
+
+```
+Access to fetch at 'https://api.example.com/user' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
+```
+
+拆解这条消息,核心就三个字:**缺 Header**。
+
+| 片段 | 含义 |
+|------|------|
+| `from origin 'https://app.example.com'` | 页面的源(协议 + 域名 + 端口) |
+| `to 'https://api.example.com/user'` | 请求的目标地址 |
+| `No 'Access-Control-Allow-Origin' header` | 服务端响应里没有这个 Header |
+
+> **思考**:如果消息变成 `Actual response does not match allowed origin`,区别在哪里?答案在 Credential 模式里。
+
+### 第二步:判断是不是真的跨域
+
+很多人一看到请求失败就认定是跨域,其实不然。先确认三个要素是否确实不同:
+
+```
+同源 = 协议相同 && 域名相同 && 端口相同
+```
+
+任何一项不同就是跨域。举几个反直觉的例子:
+
+| 页面 | 请求地址 | 跨域? | 原因 |
+|------|---------|--------|------|
+| `http://localhost:3000` | `http://localhost:8080/api` | ✅ | 端口不同 |
+| `http://localhost:3000` | `http://127.0.0.1:8080/api` | ✅ | 域名不同(localhost ≠ 127.0.0.1) |
+| `https://app.example.com` | `https://app.example.com:443/api` | ❌ | 443 是 HTTPS 默认端口,等价 |
+| `http://example.com` | `http://www.example.com/api` | ✅ | 域名不同(有 www 和没 www) |
+
+> **常见坑**:本地开发时前端走 `localhost`,后端绑在 `127.0.0.1` —— 这确实是跨域!详见 [[跨域问题调试/localhost-vs-127.0.0.1]]。
+
+#### 快速验证
+
+用浏览器直接打开目标 API 地址。如果能正常返回 JSON,说明服务端可达,问题大概率在跨域配置;如果直接 404 或连不上,是网络或服务问题,不是跨域。
+
+### 第三步:看 Network 面板的关键指标
+
+> 参考示例 -> [[跨域 HTTP 请求示例]]
+
+打开 DevTools → Network → 找到失败的请求,关注以下字段:
+
+**Request Headers 区:**
+
+| 关键字段 | 关注点 |
+|---------|--------|
+| `Origin` | 值是什么?是否和你的预期页面一致? |
+| `Referer` / `Sec-Fetch-Dest` | 帮助判断请求来源是否被代理或重写 |
+
+**Response Headers 区(最关键):**
+
+| 检查项 | 期望值 | 出问题的表现 |
+|--------|--------|-------------|
+| `Access-Control-Allow-Origin` | 具体域名或 `*` | 缺失、写错、写了 `*` 但前端带了凭证 |
+| `Access-Control-Allow-Credentials` | `true` | 缺失或为 `false`(当前端 `credentials: "include"` 时必须为 `true`) |
+| `Access-Control-Allow-Methods` | 包含你的 HTTP 方法 | 预检失败,缺少 `PUT` / `DELETE` 等 |
+| `Access-Control-Allow-Headers` | 包含你的自定义 Header | 预检失败,缺少 `X-Token` / `Authorization` 等 |
+| 状态码 | `204`(OPTIONS 预检)或 `2xx` | 预检返回 `403`/`405`/`500` 意味着中间件未生效或拦截了 |
+
+> **教学提示**:把 Network 面板顶部的 **Preserve log** 勾上——否则页面跳转后历史请求会被清掉,看不到失败的 OPTIONS 请求。
+
+#### 诊断流程图
+
+```mermaid
+flowchart TD
+ A[控制台报 CORS 错误] --> B{Network 里有 OPTIONS 请求?}
+ B -- 否 --> C["简单请求,查响应头
是否有 Access-Control-Allow-Origin"]
+ B -- 是 --> D{OPTIONS 状态码?}
+ D -- 204/200 --> E["预检通过,查实际请求响应头
是否仍缺少 CORS header"]
+ D -- 405/403/404 --> F["预检被服务端拒绝
→ 中间件未注册或路由拦截"]
+ D -- 500 --> G["服务端异常
→ 检查中间件代码 panic"]
+ C -- Header 存在 --> H["检查 Origin 匹配逻辑
动态 Origin 回显是否正确"]
+ C -- Header 缺失 --> I["服务端未设置 CORS header
→ 检查中间件顺序和执行路径"]
+ E -- Header 正确 --> J["浏览器缓存的老预检结果
→ 清除缓存或改 Max-Age"]
+ E -- Header 缺失或不匹配 --> K["实际响应链路断了
→ handler 中覆盖或重定向丢失 header"]
+```
+
+### 第四步:高频场景逐一排查
+
+#### 场景 1:Credential 模式下 `Allow-Origin: *` 报错
+
+这是最常见的生产环境跨域问题之一。
+
+**错误信息:**
+```
+Access to fetch at '...' from origin '...' has been blocked by CORS policy:
+The value of the 'Access-Control-Allow-Origin' header (*) in the response must
+not be the wildcard '*' when the request's credentials mode is 'include'.
+```
+
+**原因**:当你在 JavaScript 中设置了 `credentials: "include"`(即允许携带 Cookie),CORS 响应头里的 `Access-Control-Allow-Origin` **不能**是 `*`,必须是具体的源。
+
+```go
+// ❌ 错误写法
+c.Header("Access-Control-Allow-Origin", "*")
+c.Header("Access-Control-Allow-Credentials", "true")
+
+// ✅ 正确写法 — 动态回显请求方 Origin
+origin := c.Request.Header.Get("Origin")
+if origin != "" {
+ c.Header("Access-Control-Allow-Origin", origin)
+}
+c.Header("Access-Control-Allow-Credentials", "true")
+```
+
+> **为什么这样设计?** `*` + 凭证 = 任何网站都能带上它的 Cookie 向你的 API 发请求,等同于完全没有 CSRF 保护。浏览器强制阻断。
+
+这也是为什么 Gin 的 `cors` 中间件文档专门提到动态 Origin 的问题 — 详见 [[GIN/3-middleware/cors-registration-scope]]。
+
+#### 场景 2:预检成功但实际请求仍然报错
+
+这种情况通常是中间件只处理了 OPTIONS 而没有在实际请求的响应中注入 CORS header。
+
+回顾一下中间件的完整逻辑:
+
+```go
+func CORSMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.Method == http.MethodOptions {
+ // ← 很多新手只做了这一步
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.AbortWithStatus(http.StatusNoContent)
+ return
+ }
+ // ← 漏掉这行!实际响应的响应头里没有 CORS header
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.Next()
+ }
+}
+```
+
+预检通过了只是第一步。浏览器收到实际响应时还会**二次校验**响应头里的 CORS header 是否与当前页面 Origin 匹配。缺了这一层,前面做的全部白费。
+
+见 [[GIN/3-middleware/cors-registration-scope/cors-preflight]] 的四轮对话机制图解。
+
+#### 场景 3:handler 内部逻辑出错导致 CORS header 丢失
+
+Gin 中如果在 middleware 之后、handler 返回值之前发生重定向、JSON 渲染异常或者 panic recovery 中断了响应链,CORS header 可能不会出现在最终响应中。
+
+**排查手法**:在 middleware 最后一行加日志输出 header,确认 header 确实设上了:
+
+```go
+c.Next()
+log.Printf("Response headers: Allow-Origin=%s", c.Writer.Header().Get("Access-Control-Allow-Origin"))
+```
+
+如果日志显示 header 存在但浏览器仍然收不到,问题可能在**反向代理层**(Nginx / Cloudflare / AWS ALB)过滤了自定义 header。
+
+#### 场景 4:Nginx / 反向代理挡住了 CORS header
+
+当你使用 Nginx 做反向代理时,它默认会过滤掉上游没有明确声明的 header。
+
+```nginx
+# ❌ 上游的 CORS header 被 Nginx 丢弃
+location /api/ {
+ proxy_pass http://backend:8080;
+}
+
+# ✅ 透传必要的 CORS header
+location /api/ {
+ proxy_pass http://backend:8080;
+ add_header Access-Control-Allow-Origin $http_origin always;
+ add_header Access-Control-Allow-Credentials "true" always;
+ add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Token" always;
+ add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
+
+ if ($request_method = OPTIONS) {
+ return 204;
+ }
+}
+```
+
+> **特征信号**:用 Postman 或 curl 直接调 API 能拿到正确的 CORS header,但在浏览器中不行。这就是中间层拦截的典型表现。
+
+#### 场景 5:`Access-Control-Expose-Headers` — 你能读到响应头吗?
+
+默认情况下,跨域请求中浏览器 JavaScript **只能访问**以下 7 个响应头:
+
+```
+Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
+```
+
+如果你想在前端用 `response.headers.get('X-Total-Count')` 读取自定义头,必须在服务端设置 `Access-Control-Expose-Headers`:
+
+```go
+c.Header("Access-Control-Expose-Headers", "X-Total-Count, X-Request-Id")
+```
+
+**排查手法**:在 Console 中输入 `console.log(fetch(...).then(r => r.headers))` 观察哪些头对 JS 可见。
+
+#### 场景 6:Max-Age 缓存导致的"改了没生效"
+
+浏览器会缓存成功的 OPTIONS 预检结果,缓存时长由 `Access-Control-Max-Age` 控制:
+
+```
+Access-Control-Max-Age: 86400 ← 缓存 24 小时
+```
+
+你在服务端修改了允许的 Header 列表或方法列表,但浏览器用的是缓存的旧预检结果,所以看起来"配置不生效"。
+
+**解决方法**:
+1. 开发阶段:将 `Max-Age` 设为 `0` 或直接关浏览器缓存(DevTools → Network → Disable cache)
+2. 生产环境:修改 CORS 策略后等待缓存过期,或在部署时临时设置为 `0` 再恢复
+
+> **生活化类比**:Max-Age 就像电梯的门禁卡——有效期内不用每次刷卡,但你换岗位后门禁权限变了,还得等新卡激活。
+
+### 第五步:开发阶段快速绕过(仅限本地开发)
+
+> **警告**:以下方法仅适用于本地开发调试,绝不要用于生产环境。
+
+| 方式 | 实现 | 注意事项 |
+|------|------|---------|
+| Chrome 启动参数 | `chrome.exe --disable-web-security --user-data-dir=C:\tmp\chrome` | 需要全新 profile 隔离,避免污染正常浏览器数据 |
+| 浏览器插件 | CORS Unblock 等扩展 | 一键开关,最方便但容易忘记关闭 |
+| 前端代理 | Vite `server.proxy` / Webpack `devServer.proxy` | **推荐**,请求不跨域,无副作用 |
+
+```js
+// Vite 示例
+export default {
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ }
+ }
+ }
+}
+```
+
+请求变成 `localhost:3000/api/xxx` → Vite 转发到 `localhost:8080/api/xxx`,同源无需 CORS。
+
+### 调试清单(收藏用)
+
+遇到问题时按顺序过一遍:
+
+- [ ] Origin 是否真的跨域?(协议 / 域名 / 端口逐项核对)
+- [ ] 响应头中是否存在 `Access-Control-Allow-Origin`?
+- [ ] 若带凭证,`Allow-Origin` 是否写死了具体域名而非 `*`?
+- [ ] 若为非简单请求,OPTIONS 预检是否返回 2xx?
+- [ ] 预检通过的 Methods/Headers 是否覆盖了实际请求?
+- [ ] 实际请求的响应中是否也注入了 CORS header?
+- [ ] 如果有反向代理,header 是否被透传?
+- [ ] 是否命中了 Max-Age 缓存?试着禁用缓存重试
+- [ ] 是否需要 `Access-Control-Expose-Headers` 才能让前端读到自定义头?
+
+## 关联笔记
+
+- [[GIN/3-middleware/cors-registration-scope]] — 全局 vs 分组注册 CORS 中间件的取舍
+- [[GIN/3-middleware/cors-preflight]] — OPTIONS 预检请求的触发条件与两轮对话流程
+- [[GIN/3-middleware]] — 中间件三级作用域机制
\ No newline at end of file
diff --git a/hzh/DEV/跨域问题调试/localhost-vs-127.0.0.1.md b/hzh/DEV/跨域问题调试/localhost-vs-127.0.0.1.md
new file mode 100644
index 0000000..776c04c
--- /dev/null
+++ b/hzh/DEV/跨域问题调试/localhost-vs-127.0.0.1.md
@@ -0,0 +1,147 @@
+---
+tags:
+ - 跨域
+ - localhost
+ - 同源策略
+ - 浏览器安全
+create time: 2026-04-29 15:30
+---
+
+# localhost vs 127.0.0.1 —— 开发环境最常见的"伪同源"坑
+
+## 概述
+
+记录一个高频踩坑场景:前端访问 `localhost`,后端监听 `127.0.0.1`,浏览器却报了 CORS 错误。它们明明指向同一台机器,为什么不算同源?
+
+## 正文
+
+### 现象
+
+```
+页面: http://localhost:3000
+请求: http://127.0.0.1:8080/api/user
+结果: ❌ CORS blocked — "No 'Access-Control-Allow-Origin' header"
+```
+
+开发者百思不得其解:`localhost` 不就等于 `127.0.0.1` 吗?
+
+### 真相:浏览器不认"等价",只认"相同"
+
+浏览器的**同源策略(Same-Origin Policy)**判定标准是严格的字符串比较:
+
+```
+同源 = 协议完全相同 && 域名完全相同 && 端口完全相同
+```
+
+这里的"域名"是一个**身份标识字符串**,不做任何语义换算。`localhost` 和 `127.0.0.1` 是两个不同的字符串,所以浏览器认为这是**两个不同的源**。
+
+> **思考**:如果浏览器把 `localhost:3000` 和 `127.0.0.1:3000` 视为同源,会有什么问题?
+
+### 为什么浏览器要这样设计?
+
+这不是 bug,而是有意为之的安全决策:
+
+```mermaid
+flowchart LR
+ A["域名 = 身份标识"] --> B["a.example.com ≠ b.example.com
即使部署在同一服务器"]
+ A --> C["localhost ≠ 127.0.0.1
即使解析到同一个网卡"]
+ B --> D["最小权限原则:
网络可达 ≠ 安全可信任"]
+ C --> D
+```
+
+| 原因 | 说明 |
+|------|------|
+| **子域名隔离** | 同机运行多个子域名站点,不应互相读取对方数据 |
+| **Host Header 攻击面** | 如果有人伪造 Host 头写成 `localhost`,浏览器不能假设"IP 一样就是安全的" |
+| **一致性保证** | 如果浏览器开始做"智能换算"(IP↔域名等价),规则会变得极其复杂且容易出漏洞 |
+
+### 常见触发路径
+
+```
+前端 dev server (Vite/Webpack) → 默认 host = localhost:3000
+ ↓
+Node.js / Go / Python 后端 → 默认 bind = 127.0.0.1:8080(更安全)
+ ↓
+ 浏览器报 CORS 错误
+```
+
+| 框架 | 默认绑定地址 |
+|------|------------|
+| Vite dev server | `localhost` |
+| Webpack devServer | `localhost` |
+| Node.js `listen()` | `0.0.0.0`(全接口,但也可能绑定 `127.0.0.1`) |
+| Python Flask debug | `127.0.0.1` |
+| Gin (`r.Run(":8080")`) | `0.0.0.0`(但显式写 `r.Run("127.0.0.1:8080")` 时就是 `127.0.0.1`) |
+
+### 解决方法
+
+#### 方法一:统一域名(最推荐)
+
+让前后端用同一个名称:
+
+```bash
+# 方式 A:全部用 localhost
+前端: http://localhost:3000
+后端: http://localhost:8080
+
+# 方式 B:全部用 127.0.0.1
+前端: http://127.0.0.1:3000
+后端: http://127.0.0.1:8080
+```
+
+修改后端监听地址:
+
+```go
+// Go — 显式绑定 localhost
+r.Run("localhost:8080")
+
+// Node.js / Express
+app.listen(8080, "127.0.0.1") // 或 "localhost"
+```
+
+#### 方法二:配置 CORS(正确姿势)
+
+如果必须混用,在后端正确设置响应头:
+
+```go
+origin := c.Request.Header.Get("Origin")
+if origin != "" {
+ c.Header("Access-Control-Allow-Origin", origin)
+}
+c.Header("Access-Control-Allow-Credentials", "true")
+```
+
+详见 `[[GIN/3-middleware/cors-registration-scope]]`。
+
+#### 方法三:开发代理(最省事)
+
+用 Vite proxy 转发请求,让它变成真正的同源:
+
+```js
+// vite.config.js
+export default {
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ }
+ }
+ }
+}
+```
+
+请求流程变为:
+```
+浏览器 → localhost:3000/api/user (同源)
+ ↓ Vite devServer 转发
+ localhost:8080/api/user
+```
+
+> **生活化类比**:你家公司大门写着"大厦A座3层"(localhost),另一扇消防通道门贴着"3层回廊"(127.0.0.1)。虽然走到的是同一个办公室,但门禁系统只认牌匾上的名字——牌子不一样,就不给你刷卡。
+
+## 关联笔记
+
+- [[跨域问题调试]] — 完整的跨域排查指南
+- [[GIN/3-middleware/cors-registration-scope]] — CORS 中间件注册方案
+- [[GIN/3-middleware/cors-preflight]] — OPTIONS 预检请求机制
diff --git a/hzh/DEV/跨域问题调试/跨域 HTTP 请求示例.md b/hzh/DEV/跨域问题调试/跨域 HTTP 请求示例.md
new file mode 100644
index 0000000..2bc25f2
--- /dev/null
+++ b/hzh/DEV/跨域问题调试/跨域 HTTP 请求示例.md
@@ -0,0 +1,204 @@
+---
+tags: [CORS, 跨域, HTTP, 网络安全, 前端调试]
+create time: 2026-04-29 10:30
+---
+
+# 跨域 HTTP 请求示例 — 网易邮箱 Countly 分析请求
+
+## 概述
+
+本文档记录一次真实的浏览器 **CORS(Cross-Origin Resource Sharing)** 请求样本:来自 `mail.163.com` 向同域名下的 `countly.mail.163.com` 发起的 POST 事件追踪请求。通过分析完整的 HTTP 请求头和响应头,理解浏览器的 CORS 机制、服务端如何正确配置跨域策略,以及安全相关的首部字段含义。
+
+> [!question] 思考
+> `countly.mail.163.com` 和 `mail.163.com` 算"跨域"吗?严格来说它们属于不同子域,浏览器会将其视为不同 origin,因此仍然触发 CORS 流程。但如果服务端未配置 CORS 头,请求会被浏览器拦截——尽管这次请求实际成功返回了 `200 OK`。
+
+## 正文
+
+### 请求概览
+
+这是一个典型的**前端埋点上报**请求,使用 Countly 分析 SDK 在用户发送邮件等行为时收集事件数据。
+
+| 项目 | 值 |
+|------|----|
+| URL | `https://countly.mail.163.com/stats/i` |
+| 方法 | POST |
+| 状态码 | 200 OK |
+| Content-Type | `application/x-www-form-urlencoded` |
+| 响应内容长度 | 20 bytes(通常为一个透明像素 GIF) |
+
+### 关键请求头解析
+
+#### Origin & Referer — 跨域判定依据
+
+> [!info] 核心概念
+> 浏览器判断是否跨域的依据是 **origin = scheme + host + port**。即使同源(如都是 `.163.com`),只要 subdomain 不同就会触发预检或直接带 Origin 的请求。
+
+```
+origin: https://mail.163.com
+referer: https://mail.163.com/
+```
+
+- `origin`:由浏览器自动附加,**不可被 JS 手动设置**。服务端通过此字段决定要不要允许跨域。
+- `referer`:告诉服务端页面来源,常用于统计和鉴权。
+
+#### Sec-Fetch-* 系列 — 请求意图标识
+
+> [!tip] W3C Fetch Metadata
+> Chrome 82+ 默认发送 `Sec-Fetch-*` 首部,帮助服务器理解请求性质,可以用来防御 CSRF 等攻击。
+
+```
+sec-fetch-mode: cors ← 表明这是一个跨域请求
+sec-fetch-site: same-site ← 同站(同一一级域名 .163.com)
+sec-fetch-dest: empty ← 无特定资源类型(非图片/脚本等)
+```
+
+**Sec-Fetch-Site 取值速查:**
+
+| 值 | 含义 |
+|----|------|
+| `same-origin` | 同源请求 |
+| `same-site` | 同站(共享 eTLD+1) |
+| `cross-site` | 真正的跨站 |
+| `none` | 无 referer 的特殊情况 |
+
+> [!info] `none` 的常见触发场景
+> `sec-fetch-site: none` 等价于浏览器无法判断请求来源站点,通常出现在以下情况:
+> - `` 打开的新标签页中发起的请求
+> - 书签页面(bookmarks:)作为来源时
+> - 页面设置了 `Referrer-Policy: no-referrer` 且请求跨域
+> - Service Worker / Background Sync 等后台任务发起的请求
+> - HTTP 页面跳转到 HTTPS 资源(部分浏览器出于安全考虑丢弃 referer)
+>
+> 这些头部的派生链路:`Sec-Fetch-*` ← `document.referrer` ← 用户导航行为。referer 缺失则整组头部失效。
+
+#### 安全相关头部
+
+> [!warning] 引用站点策略
+> `Referrer-Policy: strict-origin-when-cross-origin` 意味着:
+> - 同源请求:发送完整 referer
+> - 跨域请求:只发送 origin(不包含路径和参数)
+> - HTTPS → HTTP:不发送 referer(防止泄露)
+
+### CORS 响应头解析
+
+服务端的响应中包含了关键的 CORS 控制头:
+
+```http
+access-control-allow-credentials: true
+access-control-allow-methods: GET,POST
+access-control-allow-origin: https://mail.163.com
+```
+
+| 响应头 | 作用 |
+|--------|------|
+| `Access-Control-Allow-Origin` | 指定允许访问的源,此处精确匹配 `https://mail.163.com`,而非通配符 `*` |
+| `Access-Control-Allow-Methods` | 允许的 HTTP 方法 |
+| `Access-Control-Allow-Credentials` | 允许携带 Cookie/认证凭据 |
+
+> [!important] Credentials + 通配符陷阱
+> 当 `Access-Control-Allow-Credentials: true` 时,**不能**将 `Access-Control-Allow-Origin` 设置为 `*`。这是 CORS 规范中的硬性约束,违反会导致浏览器直接报错。此处的写法是正确的:精确指定允许的 origin。
+
+### CORS 请求全流程图解
+
+#### 场景一:简单请求(无需预检)— 当前记录的情况
+
+`Content-Type` 为 `application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain` 之一时,**只发 1 次 POST**:
+
+```mermaid
+sequenceDiagram
+ participant B as 浏览器
+ participant S as countly.mail.163.com
+ participant M as mail.163.com(当前页面)
+
+ Note over B,M: JS 发起事件追踪
+ B->>S: POST /stats/i
Origin: https://mail.163.com
Content-Type: application/x-www-form-urlencoded
+ S-->>B: 200 OK
Access-Control-Allow-Origin: https://mail.163.com
Access-Control-Allow-Credentials: true
+ Note over B: ✅ 校验通过,响应交由 JS 处理
+```
+
+#### 场景二:需要预检的请求 — 常见情况
+
+当 `Content-Type` 为 `application/json`、`application/octet-stream` 等非简单类型时,浏览器先发 **OPTIONS 预检**,通过后才发实际请求:**共 2 次**。
+
+```mermaid
+sequenceDiagram
+ participant B as 浏览器
+ participant S as countly.mail.163.com
+ participant M as mail.163.com(当前页面)
+
+ Note over B,M: JS 发起带自定义 header 的跨域 POST
+ B->>S: OPTIONS /stats/i
Origin: https://mail.163.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
+ activate S
+ S-->>B: 204 No Content
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 86400
+ deactivate S
+ Note over B: ⏱ 预检通过,按 Max-Age 缓存此结果
+ B->>S: POST /stats/i
Origin: https://mail.163.com
Content-Type: application/octet-stream
+ activate S
+ S-->>B: 200 OK
Access-Control-Allow-Origin: https://mail.163.com
Access-Control-Allow-Credentials: true
+ deactivate S
+ Note over B: ✅ 校验通过,响应交由 JS 处理
+```
+
+> [!warning] 预检失败的处理
+> 如果 OPTIONS 返回非 2xx 或响应中缺少必需的 CORS 头(如 `Allow-Methods`、`Allow-Headers`),浏览器会**直接放弃发送实际请求**并抛出 CORS 错误。这意味着客户端甚至不会看到服务端的原始响应。
+
+### 代码示例
+
+以下是用原生 `fetch` API 复现此类埋点请求的代码:
+
+```typescript
+// 发送跨域埋点事件(与 Countly SDK 行为类似)
+async function trackEvent(payload: Record): Promise {
+ // 构造表单数据(服务端期望 application/x-www-form-urlencoded)
+ const body = new URLSearchParams(payload).toString();
+
+ await fetch('https://countly.mail.163.com/stats/i', {
+ method: 'POST',
+ credentials: 'include', // 携带 Cookie(对应 Allow-Credentials: true)
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body,
+ // keepalive: true 用于 pagehide 时保证请求发出(可选优化)
+ });
+}
+
+// 调用示例
+trackEvent({
+ event_key: 'email_sent',
+ count: '1',
+ duration: '120',
+});
+```
+
+> [!note] 解释
+> 上述代码中 `credentials: 'include'` 是关键:它让浏览器在跨域请求中携带 Cookie,这要求服务端必须明确返回 `Access-Control-Allow-Credentials: true` 且 `Access-Control-Allow-Origin` 必须是具体域名(不能是 `*`)。
+
+### 其他请求头一览
+
+其余头部为常规 HTTP 标准字段,供参考:
+
+```
+:authority countly.mail.163.com # H2 pseudo-header,目标主机
+:method POST # HTTP/2
+:path /stats/i # 请求路径
+:scheme https # 协议
+
+accept */* # 接受任意内容类型
+accept-encoding gzip, deflate, br, zstd # 支持的压缩算法
+accept-language zh-CN,zh;q=0.9,... # 语言偏好
+content-length 2184 # 请求体大小
+priority u=1, i # HTTP/3 优先级
+user-agent Mozilla/5.0 ... Edg/147 # 浏览器标识
+
+sec-ch-ua "Microsoft Edge";v="147" # UA 品牌信息
+sec-ch-ua-mobile ?0 # 非移动端
+sec-ch-ua-platform "Windows" # 操作系统
+```
+
+> [!info] 关于 HTTP/2
+> 此请求使用了 HTTP/2(存在 `:authority`、`:method` 等伪头),HTTP/2 通过多路复用避免了传统 HTTP/1.1 的队头阻塞问题,对同时加载多个资源的现代 Web 应用性能提升明显。
+
+## 关联笔记
+
+- [[localhost-vs-127.0.0.1]]
diff --git a/hzh/TEST/gin.md b/hzh/TEST/gin.md
index e230b65..c0660bf 100644
--- a/hzh/TEST/gin.md
+++ b/hzh/TEST/gin.md
@@ -1,5 +1,10 @@
---
-tags: [后端, Go, Gin, 测试, 自我考察]
+tags:
+ - 后端
+ - Go
+ - Gin
+ - 测试
+ - 自我考察
create time: 2026-04-28
update time: 2026-04-28
status: reviewed