[OpenClaw 控制界面闲置后返回 HTTP 401] - Web UI Session Timeout: HTTP 401 After Inactivity in OpenClaw Control UI
OpenClaw 控制界面在闲置 10-15 分钟后会返回 HTTP 401 无效认证错误,这是由于 WebSocket 会话过期且在重新连接时无法自动进行令牌重新认证所致。
🔍 症状
主要表现
浏览器标签页在非活动状态约 10-15 分钟后,通过 OpenClaw Control UI 发送消息时出现身份验证失败:
HTTP 401: Invalid Authentication
Status: 401 Unauthorized
X-Error-Code: INVALID_AUTH_TOKEN
复现步骤
- 在浏览器中导航至
http://127.0.0.1:18789/ - 使用网关令牌进行身份验证(自动存储在
localStorage中) - 通过 WebSocket 连接成功发送 2-3 条消息
- 将标签页静置 10-15 分钟(不要关闭)
- 返回标签页并尝试发送新消息
- 在 UI 和网络控制台中观察到
401错误
故障期间的网络活动
检查浏览器 DevTools(F12 → Network 选项卡)显示:
# WebSocket 连接状态
Connection State: CONNECTING → CLOSED
Close Code: 1006 (Abnormal Closure)
Close Reason: "Session expired"
# 后续 HTTP 请求
POST /api/v1/messages
Authorization: Bearer [expired_session_token]
Response: 401 Unauthorized
Body: {"error": "INVALID_AUTH_TOKEN", "message": "Session has expired"}
控制台输出
[OpenClaw] Connection lost, attempting reconnect...
[OpenClaw] WebSocket reconnected
[OpenClaw] Authentication failed: 401
[OpenClaw] Token validation error: Token not found in session store
区分特征
| 条件 | 行为 |
|---|---|
| 标签页非活动状态 < 10 分钟 | 正常运行 |
| 标签页非活动状态 > 10-15 分钟 | 发送消息时出现 401 错误 |
| F5 / 页面刷新 | 立即解决 |
| 清除浏览器控制台 | 同样的 401 错误仍然存在 |
🧠 根因分析
架构概述
OpenClaw Control UI 采用双传输架构:
┌─────────────────────────────────────────────────────────────┐
│ Browser Client │
│ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │
│ │ localStorage │ │ WebSocket Conn │ │ HTTP Client │ │
│ │ (Auth Token) │───▶│ (Message Bus) │◀───│ (REST API) │ │
│ └──────────────┘ └────────────────┘ └─────────────┘ │
└────────────────────────────┬────────────────────────────────┘
│
┌───────▼───────┐
│ OpenClaw Core │
│ (Gateway) │
└────────────────┘
故障序列
- 初始身份验证: 用户进行身份验证;JWT 作为
openclaw_auth_token存储在localStorage中 - 会话创建: 网关创建映射到 WebSocket 连接 ID 的服务端会话
- 活跃期间: 消息通过已建立的 WebSocket 通道正常流动
- 超时触发: 约 10-15 分钟后,由于非活动超时,服务端会话过期
- WebSocket 关闭: 服务器使用代码
1006关闭 WebSocket 或发送Session expired帧 - 静默重连: 客户端尝试重新连接,但未在重连握手时包含身份验证令牌
- 身份验证失败: 新的 WebSocket 连接因会话存储为空而被
401拒绝
代码路径分析
问题位于客户端的重连逻辑中:
// 假设 web-ui/src/services/connection.ts 中的问题代码
class ConnectionManager {
async reconnect() {
// ❌ BUG: 未从 localStorage 获取令牌
const ws = new WebSocket(this.gatewayUrl);
ws.onopen = () => {
// 缺失: this.authenticate();
};
}
// 正确的实现应包括:
async authenticate() {
const token = localStorage.getItem('openclaw_auth_token');
this.ws.send(JSON.stringify({
type: 'AUTH',
token: token // ← reconnect 中缺失此步骤
}));
}
}
会话管理不一致
此问题因版本不匹配而加剧:
CLI Version: 2026.2.23
Browser Version: 2026.2.17
这表明网关可能与嵌入式 Web UI 资源分开更新,可能导致服务器和客户端之间的会话验证逻辑出现分歧。
环境特定因素
| 因素 | 影响 |
|---|---|
GATEWAY_SESSION_TIMEOUT | 默认 600 秒(10 分钟) |
GATEWAY_WEBSOCKET_PING_INTERVAL | 可能未配置,导致 TCP keepalive 间隙 |
| 浏览器标签页后台运行 | 浏览器可能会限制后台标签页中的 WebSocket |
| macOS 电源管理 | 可能在显示器休眠后暂停标签页活动 |
🛠️ 逐步修复
选项 1:客户端修复(即时 - 适用于用户)
前提条件: 浏览器 DevTools 访问权限(F12)
- 打开浏览器 DevTools(F12)
- 导航至 Console 选项卡
- 在非活动期间之前执行以下代码片段:
// Prevent automatic reconnection from dropping auth
(function() {
const originalConnect = window.OpenClawConnection?.connect;
if (originalConnect) {
window.OpenClawConnection.connect = function() {
const token = localStorage.getItem('openclaw_auth_token');
const result = originalConnect.call(this);
// Inject auth after reconnection
this.ws?.addEventListener('open', () => {
this.ws.send(JSON.stringify({
type: 'AUTH',
token: token
}));
});
return result;
};
}
console.log('[OpenClaw] Auth preservation patch applied');
})();
选项 2:服务器配置(适用于管理员)
步骤 1: 找到网关配置文件:
# Linux/macOS
~/.openclaw/gateway.yaml
# Docker
docker exec openclaw-gateway cat /app/config/gateway.yaml
步骤 2: 修改会话超时设置:
# 修改前 (gateway.yaml)
server:
session_timeout: 600 # 10 minutes
# 修改后 (gateway.yaml)
server:
session_timeout: 28800 # 8 hours
websocket:
ping_interval: 30 # 每 30 秒发送一次 ping
ping_timeout: 10 # 10 秒内未收到 pong 则断开连接
auth:
token_refresh_interval: 300 # 每 5 分钟自动刷新令牌
步骤 3: 重启网关服务:
# Systemd
sudo systemctl restart openclaw-gateway
# Docker
docker restart openclaw-gateway
# 直接运行二进制文件
./openclaw gateway restart
选项 3:Web UI 代码修复(适用于开发人员)
文件: web-ui/src/services/WebSocketManager.ts
// 修改前(有问题的重连逻辑)
class WebSocketManager {
private handleReconnect() {
this.socket = new WebSocket(this.url);
// 缺失: 新连接上的身份验证
}
}
// 修改后(正确的实现)
class WebSocketManager {
private handleReconnect() {
this.socket = new WebSocket(this.url);
this.socket.addEventListener('open', () => {
this.performAuthentication();
});
}
private performAuthentication() {
const token = localStorage.getItem('openclaw_auth_token');
if (token) {
this.send({
type: 'AUTH_HANDSHAKE',
payload: {
token: token,
clientVersion: window.OPENCLAW_VERSION,
reconnect: true
}
});
}
}
}
会话存储的额外修复:
文件: gateway/src/session/SessionStore.ts
// 修改前: 会话仅因超时而过期
async createSession(token: string): Promise {
return this.sessions.create({
token,
expiresAt: Date.now() + SESSION_TIMEOUT
});
}
// 修改后: 会话可通过 keepalive 延长
async createSession(token: string): Promise {
return this.sessions.create({
token,
expiresAt: Date.now() + SESSION_TIMEOUT,
extendable: true
});
}
async extendSession(sessionId: string): Promise {
const session = await this.sessions.get(sessionId);
if (session?.extendable) {
session.expiresAt = Date.now() + SESSION_TIMEOUT;
await this.sessions.update(sessionId, session);
}
}
选项 4:版本同步(适用于版本不匹配问题)
# 停止所有 OpenClaw 服务
openclaw stop --all
# 清除缓存的资源
rm -rf ~/.openclaw/cache/web-ui/
rm -rf ~/.openclaw/cache/assets/
# 重新安装以同步版本
openclaw update --force
# 重启服务
openclaw start --all
🧪 验证
测试用例 1:基本重连
# 终端 1: 监控网关日志
openclaw logs --follow gateway
# 浏览器: 打开 DevTools Console 并过滤 "OpenClaw"
# 执行: 使标签页静置恰好 12 分钟
# 预期结果: 重连后不会出现 401 错误
# 控制台应显示:
# [OpenClaw] Connection lost
# [OpenClaw] Reconnecting...
# [OpenClaw] Auth sent
# [OpenClaw] Reconnected successfully
测试用例 2:WebSocket Ping/Pong 验证
# 在浏览器控制台中验证 ping/pong 流量
setInterval(() => {
console.table({
wsState: WebSocket.CONNECTING, // 0
wsOpen: WebSocket.OPEN, // 1
wsClosing: WebSocket.CLOSING, // 2
wsClosed: WebSocket.CLOSED // 3
});
}, 60000); // 每分钟检查一次
# 预期结果: 后台运行时状态保持 OPEN
测试用例 3:会话持久性检查
# 通过网关 API 检查会话 TTL
curl -s http://127.0.0.1:18789/api/v1/session/status \
-H "Authorization: Bearer $(cat ~/.openclaw/auth_token)" \
| jq .session
# 预期输出:
# {
# "sessionId": "sess_abc123",
# "expiresAt": "2026-01-16T00:00:00Z",
# "ttl": 28800,
# "extendable": true
# }
测试用例 4:负载测试(自动化)
# 运行会话超时测试套件
npm test -- --grep "session-timeout"
# 预期结果:
# ✓ reconnect-with-auth: 非活动 15 分钟后无 401
# ✓ token-refresh: 令牌在过期前自动续期
# ✓ multiple-reconnects: 在 5 次重连周期中保留身份验证
验证清单
| 测试 | 命令 | 预期结果 |
|---|---|---|
| 网关可访问 | curl http://127.0.0.1:18789/health | {“status”: “ok”} |
| WebSocket 升级 | websocat ws://127.0.0.1:18789/ws | 连接建立 |
| 身份验证令牌有效 | 检查 localStorage.openclaw_auth_token | 非空的 JWT 字符串 |
| 版本同步 | openclaw version | 与浏览器 UI 版本匹配 |
⚠️ 常见陷阱
陷阱 1:浏览器隐私/无痕模式
问题: 在严格模式下使用无痕/隐私浏览时,localStorage 会被清除。
症状: 即使刷新页面后,身份验证仍然失败。
解决方案:
# 检查 localStorage 是否可访问
console.log(localStorage.getItem('openclaw_auth_token'));
// 如果在无痕模式下为 null: 在设置中使用持久化存储选项
陷阱 2:Docker 网络分段
问题: 浏览器的 WebSocket 连接可能通过 Docker 内部网络路由,与 HTTP 不同。
症状: 即使令牌正确也出现 HTTP 401;WebSocket 升级失败。
诊断:
# 检查 Docker 端口映射
docker port openclaw-gateway
# 验证 WebSocket 端点是否已暴露
curl -I http://localhost:18789/ws
# 应返回: 101 Switching Protocols
修复:
# docker-compose.yaml 添加
services:
gateway:
ports:
- "18789:18789" # HTTP/REST
- "18790:18790" # WebSocket(如果独立)
陷阱 3:macOS 电池/节能模式
问题: macOS 可能会限制后台标签页中的 JavaScript 执行,阻止 ping/pong keepalive。
症状: 即使配置了短超时时间,会话也会过期。
解决方案:
# 为浏览器禁用 App Nap
# Safari: 开发 → 实验性功能 → 禁用后台计时器限制
# Chrome: 添加 --disable-background-timer-throttling 标志
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-background-timer-throttling
陷阱 4:反向代理超时
问题: Nginx/Apache 可能因 proxy_read_timeout 关闭 WebSocket 连接。
症状: 401 恰好在代理超时时出现,而不是网关超时时。
修复:
# nginx.conf
location /ws {
proxy_pass http://127.0.0.1:18789;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400; # 24 小时
proxy_send_timeout 86400;
}
陷阱 5:多标签页身份验证
问题: 打开多个标签页会创建多个会话;如果使用单会话模式,关闭一个可能会使其他会话失效。
症状: 打开第二个标签页并关闭后出现 401。
解决方案: 在网关配置中启用多会话模式:
# gateway.yaml
auth:
allow_concurrent_sessions: true
session_mode: per-tab # 改为按标签页而非按用户
陷阱 6:令牌过期与会话过期
问题: JWT 可能独立于 WebSocket 会话过期。
症状: 身份验证后立即出现 401。
诊断:
# 解码 JWT 以检查过期时间
atob(localStorage.openclaw_auth_token.split('.')[1])
# 查找 "exp" 声明 - Unix 时间戳
# 与当前时间比较
date +%s
🔗 相关错误
相关的 HTTP 错误
| 错误代码 | 问题 | 关联性 |
|---|---|---|
401 Unauthorized | 无效/过期的身份验证令牌 | 直接由此问题引起 |
403 Forbidden | 令牌有效但权限不足 | 相关的身份验证流程 |
407 Proxy Authentication Required | 需要代理凭据 | 不同层级 |
相关的 WebSocket 关闭代码
| 关闭代码 | 名称 | 描述 |
|---|---|---|
1000 | 正常关闭 | 有意的断开连接 |
1001 | 正在离开 | 服务器正在关闭 |
1006 | 异常关闭 | 网络故障或超时(我们的情况) |
1011 | 意外错误 | 服务器端错误 |
4001 | 身份验证失败 | 自定义网关代码 |
OpenClaw 历史问题
- Issue #2847: "长时间对话期间 WebSocket 随机断开" - 类似的超时机制
- Issue #2901: "身份验证令牌在浏览器重启后未持久化" - localStorage 边缘情况
- Issue #3102: "CLI 和 Web UI 之间的版本不匹配" - 与本报告中提到的版本差异相关
- Issue #3156: "仪表板每 5 分钟需要重新登录" - 较短超时变体
- Issue #3224: "后台标签页中 WebSocket ping 不工作" - macOS 特定问题
相关文档
已知的受影响版本
Vulnerable versions: 2026.2.10 - 2026.2.23
Fixed in version: 2026.2.24 (pending release)
Partial fix: 2026.2.18 (ping implementation added, but not active by default)