# 98-治本:陈旧 `dsh-auth-*` cookie 回写清理(2026-09-14 落地) - 日期:2026-09-14 | 状态:✅ **已上线**(平台后端已重建重启,端到端实测通过) - 触发:用户「**必须要治本**」(承档案 97 之后查明的 431 故障根因) > **TL;DR** > dsh **每次实例 (重)启动都换** `dsh-auth-<随机后缀>` 的 **cookie 名**;平台此前只在**转发给实例时** > 丢弃旧的(`mergeCookieHeader`),**从不回写浏览器** ⇒ 浏览器 jar **只增不减** ⇒ `Cookie` 请求头 > 越涨越长 ⇒ 涨到 **~15 KB** 就被 Cloudflare / 本机 nginx 直接 **431** 拒掉 ⇒ **请求根本到不了平台** > ⇒ 那条 11 MB 合并脚本取不到 ⇒ 客户端报 `bundle script … failed to load`(界面「Failed to load plugins」)。 > 修法:**在响应里对陈旧的名字回写 `Set-Cookie: …; Max-Age=0`** ⇒ 浏览器把它们丢掉 ⇒ jar 不再增长。 --- ## 一、实测证据(本机对线上做的) | `Cookie` 请求头大小 | 结果(直连 nginx:443) | |---|---| | 12,769 B | **401** ← 放行到平台 | | **19,149 B** | **431** | | 25,529 B | **431** | ⇒ 阈值落在 **13–19 KB**;⚠️ 19 KB 时**连 `/` 也 431** ⇒ 一过线就是"整站请求被拒",用户看到"立即报错、什么都没加载"。 **旁证(三条反常现象全对上)**:平台日志里**没有**那条 bundle 请求(请求被 CF/nginx 拒在门外)✓; **无痕正常**(空 cookie 罐 ⇒ 头只有几百字节)✓;服务器侧一切正常(dsh 启动无错、无 OOM、产物语法通过)✓。 📌 **Cloudflare 官方 changelog(2025-10-16)**:此前「总 32 KB / 单头 16 KB」,之后提到 128 KB ⇒ 本轮 431 **更可能来自本机 nginx**(实测 19 KB 即 431),CF 只是把响应透传回来。 ## 二、改了什么(只动 `proxy.ts` 一处) 在响应头块(重放 cookie 合并之后、`writeHead` 之前)加一段: ```ts // 只在「本次响应确实下发了 dsh-auth-*」时才动手 —— 那时才确知当前有效的名字 const scList = Array.isArray(scNow) ? scNow : scNow === undefined ? [] : [String(scNow)] const freshNames = scList.map(取名字).filter(Boolean) if (freshNames.length > 0) { const staleNames = (request.headers.cookie ?? '') .split(';').map(取名字).filter((n) => n.startsWith('dsh-auth-') && !freshNames.includes(n)) if (staleNames.length > 0) { headers['set-cookie'] = [...scList, ...staleNames.map((n) => `${n}=; Path=/; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`)] } } ``` **为什么这样判据是安全的**: - 只有"实例本次下发了 `dsh-auth-*`"(= 我们知道当前有效名字)时才清; - 其余情形(普通 API 响应不带该 cookie)**什么都不做** —— 绝不误删正在用的那个; - **不带 `Domain=`**(dsh 下发的是 host-only cookie,带 Domain 反而删不掉); - 即使判断有误,档案 51 的「401 → 取新 cookie 透明重放」也会自愈。 `scripts/verify-inject.cjs` 增加防回退断言:「proxy.ts 会清理陈旧 dsh-auth cookie」。 ## 三、验证 | 层 | 手段 | 结果 | |---|---|---| | 类型/构建/单测 | `npm run typecheck` + `npm run verify` + 服务器 `ci.sh` | ✅ EXIT=0 全绿;✅ CI OK(48 pass / 0 fail) | | 断言 | `node scripts/verify-inject.cjs` | ✅ 新增项通过 | | **端到端(最强)** | 临时 session(R4)→ `/api/dsh/enter` 取带 token 的 URL → **带 2 个假 `dsh-auth-*`** 请求它 | 303 响应含 **3 条 Set-Cookie**,其中 **2 条 `Max-Age=0`** 正是那两个假名字 ✅ | ## 四、⚠️ 它救不了"当下",只防复发 **已经 431 时,请求在 CF/nginx 就被拒了、到不了平台** ⇒ 平台没法回写清理指令。 ⇒ **用户必须先手动清一次**(F12 → Application → Cookies → 只删名字以 `dsh-auth-` 开头的,**保留 `sid`**); 此后由本机制维持小 jar。 ## 五、遗留 / 后续 - **根因仍存在**:dsh 官方每(重)启动换 cookie 名(我们不改官方)。本机制只是**跟着清**。 ⇒ 推论:**频繁重启实例会持续制造新 cookie**(今天为部署重启十几次即为此例)——仍是"少重启"的又一个理由。 - 可选加固(未做):nginx 的 `large_client_header_buffers` 已是 `4 32k`,可不动;**CF 侧限额不可配**, 所以"保持小 jar"是唯一可靠路径。若日后仍偶发 431,可考虑在平台侧**直接把超过 N 个的 `dsh-auth-*` 从请求里剥掉**(转发时已有 `mergeCookieHeader` 丢弃逻辑,只是要加"超量"判据)。 - 回滚:`git revert <本次 commit>` → `npm run build` → `systemctl restart dshs`; 备份 `/opt/dsh/backups/pre-99-20260914-225704/`。