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

156 lines
5.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
**根因**
```ts
// 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`
```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 成功后刷新:
```ts
onSelect(matchID, joinRes.playerID, joinRes.playerCredentials);
await refresh(); // ← 新增:让 "加入 P1" 按钮自动隐藏
```
2. `apps/web/e2e/war-multiplayer.spec.ts` — 选"含加入 P1 按钮的房间"而不是 `.first()`
```ts
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
```ts
// 之前:断言一定是 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 });
```
## 怎么解决(部署命令)
```bash
# 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` 在 `:8000`vite 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. 两边都能进游戏 → 一边翻牌 → 另一边同步
## 关联
- [09-room-list-ui-and-complete-tests.md](./09-room-list-ui-and-complete-tests.md) — 上一轮的初始实现
- [lessons-learned.md §9.1](./lessons-learned.md) — '*' 不通配;新加 §10.1-10.3
- [decisions.md D24](./decisions.md) — dev origins 覆盖 LAN