Files
huajishe-tts/docs/dev-log/10-cors-and-join-bugs.md
e2hang 1956c23502 fix(multiplayer): LAN CORS + join refresh + tie-tolerant tests
User-tested LAN deployment found two bugs:

1. CORS: dev origins regex only matched hostname 'localhost', so LAN IP
   192.168.5.11 was silently rejected by @koa/cors. Expanded the default
   RegExp list to cover 192.168.x.x / 10.x.x.x / 172.16-31.x.x /
   100.x.x.x (Tailscale/CGNAT).

2. 409 on "加入 P1": joinRoom didn't refresh the local rooms state, so
   after a successful join the "加入 P1" button was still visible.
   Re-clicking then returned 409 from the server. Added await refresh()
   in joinRoom (both success and error paths).

Also: E2E was flaky when shuffle produced a tie (collect keeps pile on
tie, by design from dev-log 09). Loosened assertion to /^[01]$/.

Added regression test 'after join, lobby list refreshes so "加入" button
disappears (no double-click 409)'.

Plus docs/dev-log/10-cors-and-join-bugs.md, lessons §10.1-10.5,
decisions D24-D26.
2026-08-23 20:47:22 +08:00

5.6 KiB
Raw Permalink Blame History

10 · 部署后修复LAN CORS + 重复 join 409 + 测试容忍 tie

做什么

把 09 那一轮交付的功能在真实 LAN 部署上跑通:

  • 服务端跑在 192.168.5.11:8000 + vite dev 192.168.5.11:5173
  • 本机 + 局域网其它机器两个浏览器都能开网页 → 选 War → 联机 → 创房间/加房间
  • 用户实测发现两个 bug已修

遇到什么问题

问题 1跨 LAN IP 访问 → fetch 失败 + 一直 connecting

症状:用户在另一台机器开 http://192.168.5.11:5173/,点 "创建房间",浏览器报:

Access to fetch at 'http://localhost:8000/games/War?isGameover=false'
from origin 'http://192.168.5.11:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

socket.io 也连不上(页面底部一直显示 connecting

根因

// 09 那轮的 dev 默认 origins
function parseOrigins(): (string | RegExp)[] {
  if (!process.env.ALLOWED_ORIGINS) {
    return [
      /^https?:\/\/localhost(:\d+)?$/,    // ← 只匹配 hostname 'localhost'
      /^https?:\/\/127\.0\.0\.1(:\d+)?$/,
      'tauri://localhost',
      'http://tauri.localhost',
    ];
  }
  ...
}

192.168.5.11 不在白名单 → isOriginAllowed 返回 false → @koa/cors 不发 Access-Control-Allow-Origin → 浏览器拦截响应。

修法packages/server/src/index.ts

return [
  /^http:\/\/localhost(:\d+)?$/,
  /^http:\/\/127\.0\.0\.1(:\d+)?$/,
  /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}(:\d+)?$/,   // LAN
  /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/, // LAN
  /^http:\/\/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}(:\d+)?$/, // LAN
  /^http:\/\/100\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/, // Tailscale/CGNAT
  /^https:\/\/localhost(:\d+)?$/,
  'tauri://localhost',
  'http://tauri.localhost',
  'https://tauri.localhost',
];

验证

$ curl -I -X OPTIONS http://192.168.5.11:8000/games/War/create \
    -H "Origin: http://192.168.5.11:5173" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: content-type"
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://192.168.5.11:5173
Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE,PATCH
Access-Control-Allow-Headers: content-type

问题 2点 "加入 P1" → HTTP 409 "Player 1 not available"

症状:用户能看到房间,但点 "加入 P1" 后 server 返回 409。

根因(两重)

(2a) 09 那轮修复时漏了:LobbyRoomList.joinRoom 成功后没有刷新列表。本地 rooms state 残留旧数据,"加入 P1" 按钮还显示着,但服务端 P1 已经被占 → 重复点就 409。

(2b) 测试/真人都可能碰上server InMemory DB 残留上一次测试的旧房间。lobby.refresh()db.listMatches() 顺序返回——插入顺序——所以 .first() 选中的可能是已满的旧房间。

修法

  1. packages/ui/src/LobbyRoomList.tsx — join 成功后刷新:

    onSelect(matchID, joinRes.playerID, joinRes.playerCredentials);
    await refresh();   // ← 新增:让 "加入 P1" 按钮自动隐藏
    
  2. apps/web/e2e/war-multiplayer.spec.ts — 选"含加入 P1 按钮的房间"而不是 .first()

    const firstJoinable = page.locator('[data-testid^="lobby-join-1-"]').first();
    
  3. 新增回归测试 after join, lobby list refreshes so "加入" button disappears——验证 joinRoom 的 refresh() 行为。

问题 3测试遇到 tie 就死

症状:跑 war E2E 偶发失败 —— collect 后 pile 还是 1。

根因

  • 09 修了 War 平局时 collect 保留 pile 的 bugdev-log 09 §问题 4
  • 服务端 Math.random() 洗牌不可预测,测试 deck 偶发平局 → collect 不收 → 测试断言 pile===0 失败

修法apps/web/e2e/war-multiplayer.spec.ts):让测试容忍 tie

// 之前:断言一定是 0
await expect(p0.getByTestId('zone-count-p0-pile')).toHaveText('0', { timeout: 10_000 });

// 现在:断言是 0 或 1tie 留着)
await expect(p1.getByTestId('zone-count-p0-pile')).toHaveText(/^[01]$/, { timeout: 10_000 });

怎么解决(部署命令)

# 1. build server
pnpm --filter @tts-like/server build

# 2. 起 server绑定 0.0.0.0 让 LAN 可达)
cd /home/e2hang/code/Projects/boardgame/tts-like/packages/server
nohup node --import tsx src/index.ts > /tmp/tts-server.log 2>&1 &

# 3. 起 vite绑定 0.0.0.0
cd /home/e2hang/code/Projects/boardgame/tts-like/apps/web
nohup pnpm dev --host 0.0.0.0 > /tmp/tts-web.log 2>&1 &

成果

  • Commit: 即将提交(fix(multiplayer): LAN CORS + join refresh + tie-tolerant tests
  • 4/4 E2E 通过(含新加的 after join ... no double-click 409 回归测试)
  • server PID 1339755:8000vite PID 1279130:5173

验证(已 curl

来源 web server /games CORS preflight
localhost
192.168.5.11 (新增 Allow-Origin: 192.168.5.11:5173)

你的验收路径

  1. 本机浏览器开 http://192.168.5.11:5173/ (强制刷新 Ctrl+Shift+R 清缓存)
  2. 选 War → 联机 → 创建房间 → 看到 "(你: 0)"
  3. 另一台机器开 http://192.168.5.11:5173/ → 选 War → 联机 → 刷新列表 → 加入 P1
  4. 两边都能进游戏 → 一边翻牌 → 另一边同步

关联