Files
dsh_shenxian/dsh-server-docs/04-调整方案/141-实例子域冷启动窗口代理裸断连接致502.md
T
admin 1242d07db0 fix(实例代理): 冷启动窗口转发失败改为 503,消除实例子域 502(档案 141)
根因:proxyHttp() 在转发两次失败后 reply.raw.destroy(),不写任何响应头与 body,边缘 nginx 只能回 502。
现象:admin 首次用 admin.ai1net.com 进场即 502 —— /api/dsh/enter 拉起实例后 12 秒访问,实例进程已在、端口已分配但 dsh 尚未监听。
改法:新增 replyUpstreamUnavailable(),响应头未发出时回 503 + Retry-After(导航请求给会自动重试的极简 HTML),仅 headersSent/writableEnded 才允许 destroy。
实测:门户 200 / admin.ai1net.com 401 / 实例端口 401 / 启动日志 0 error / verify-inject 全绿。
2026-09-19 16:18:41 +08:00

4.7 KiB
Raw Blame History

141-实例子域冷启动窗口内代理裸断连接致 502(2026-09-19 修复)

一句话:实例已拉起、端口已分配,但 dsh 还没开始监听的那十几秒里,平台代理转发两次都失败后 不写任何响应就销毁连接 ⇒ 边缘 nginx 只能回 502(浏览器拿到一张无信息的错误页)。 现改为回 503 + Retry-After(导航请求给一页会自动重试的极简 HTML)。

现象(用户报障)

「admin 账号登录服务器报错 502」——admin 登录门户成功,随后打开自己的工作区 https://admin.ai1net.com/ 时页面 502。

取证链(全部实测)

# 证据 内容
1 nginx error log 2026/09/19 16:07:04 [error] upstream prematurely closed connection while reading response header from upstream, request: "GET / HTTP/2.0", upstream: "http://127.0.0.1:3080/", host: "admin.ai1net.com";16:07:05 GET /favicon.ico 同错
2 nginx access log host=admin.ai1net.com 全程只有这 2 条、都是 502(说明是该域首次被访问)
3 平台 journal req-1tx GET / (host=admin.ai1net.com) 只有 incoming request、没有 request completed ⇒ 平台接了请求但没写出任何响应
4 触发时序 16:06:52 POST /api/auth/login 200(80 ms) → 16:06:52 POST /api/dsh/enter 200 但耗时 11.1 s → scope dsh-114801-70e1d05b.scope ActiveEnterTimestamp=16:06:52 → 16:07:04 用户访问(= 实例启动后 12 秒)
5 现状自愈 curl 127.0.0.1:20000 = 401(dsh 正常态)⇒ 实例就绪后自行恢复;期间同机 curl 带 Host: admin.ai1net.com 打 3080 一律 401,无法复现 502

⚠️ 该 502 不是平台进程挂了(dshs 一直 active、NRestarts=0、3080 在听、日志全是 200), 也不是域名/nginx 配置问题 —— 别往这三个方向查。

根因(代码级)

src/supervisor/proxy.ts → proxyHttp():

  • upstream.on('error'):首次失败 → 丢弃 keep-alive 池并重试一次;第二次失败(connRetry === true)→ reply.raw.destroy()(旧 517–520 行)
  • 同类:upRes.on('error', () => reply.raw.destroy())(旧 514 行)

⇒ 两处都是不发响应头、不写 body,直接销毁 socket,客户端一侧只能由边缘代理兜底成 502。

为什么没走"实例未运行"的过渡页分支:resolveSubdomainAccess() 用 app.supervisor.endpointFor(userId) 判活(不读 dsh_instances.status)。实例记录/端口已存在 ⇒ 返回 endpoint,于是进的是转发分支而不是 404 not_running → 302 /wake.html(档案 49 那条)—— 用户既拿不到过渡页、也拿不到 503,只有 502。

修法(最小改动,只动错误分支)

新增模块级 replyUpstreamUnavailable(request, reply):

  • 响应头尚未发出 ⇒ 导航请求(GET + accept: text/html)回 503 + Retry-After: 2 + 会自动 location.reload() 的极简 HTML; 非导航回 503 + JSON {"error":"instance_starting"}(与既有 652 行口径一致)
  • 已 headersSent / writableEnded ⇒ 维持 destroy()(不能在半截响应上再写头)

调用点:upstream.on('error') 的 connRetry 分支、upRes.on('error')。

部署与回滚

项 内容
送法 本机 tsc -p tsconfig.json → scp lib/supervisor/proxy.js → systemctl restart dshs(只送编译产物)
送前核对 已 diff 线上文件 vs 本地新产物 = 仅本次 43 行差异(证明线上 = 仓库 HEAD 的产物)
备份 /opt/dsh/backups/proxy.js.pre141-20260919-161444(37350 B)
回滚 cp 该备份回 /opt/dshs/lib/supervisor/proxy.js → systemctl restart dshs
验收 is-active dshs=active | 门户 Host: ai1net.com = 200 | admin.ai1net.com = 401 | 127.0.0.1:20000 = 401 | 启动日志 0 error | verify-inject.cjs 全绿

遗留(未修,另行立项)

  1. 🟡 dsh_instances 里 admin 行 status=stopped、pid/port 为空,而 47 上它的 scope 一直在跑 ⇒ 控制面视图与实际进程不一致。本次修复不依赖该字段(endpointFor 另有来源),故不影响; 但是否诱发**重复 launch**需单独立项核。
  2. ⚪ WebSocket upgrade 分支的 socket.destroy()(684 行,未认证 / 未知 host)属预期行为,未动。

关联

  • 档案 49:实例未就绪 → 过渡页 /wake.html 的既有设计(本档案是它在"实例已存在但未监听"这一盲区的补漏)
  • PLAYBOOK-实例与插件坑.md §18:同为「报错形态误导排查方向」族 —— 报 502 时先查"平台有没有回响应",别先怀疑 nginx / 域名 / 实例挂了