Web UI Session Timeout: HTTP 401 After Inactivity in OpenClaw Control UI
The OpenClaw Control UI returns HTTP 401 Invalid Authentication after 10-15 minutes of inactivity due to WebSocket session expiration without automatic token re-authentication on reconnect.
π Symptoms
Primary Manifestation
After approximately 10-15 minutes of browser tab inactivity, attempting to send a message via the OpenClaw Control UI results in an authentication failure:
HTTP 401: Invalid Authentication
Status: 401 Unauthorized
X-Error-Code: INVALID_AUTH_TOKEN
Reproduction Sequence
- Navigate to
http://127.0.0.1:18789/in browser - Authenticate with gateway token (automatically stored in
localStorage) - Send 2-3 messages successfully via WebSocket connection
- Leave tab inactive for 10-15 minutes (do not close)
- Return to tab and attempt to send a new message
- Observe
401error in UI and network console
Network Activity During Failure
Examining browser DevTools (F12 β Network tab) reveals:
# WebSocket connection state
Connection State: CONNECTING β CLOSED
Close Code: 1006 (Abnormal Closure)
Close Reason: "Session expired"
# Subsequent HTTP requests
POST /api/v1/messages
Authorization: Bearer [expired_session_token]
Response: 401 Unauthorized
Body: {"error": "INVALID_AUTH_TOKEN", "message": "Session has expired"}
Console Output
[OpenClaw] Connection lost, attempting reconnect...
[OpenClaw] WebSocket reconnected
[OpenClaw] Authentication failed: 401
[OpenClaw] Token validation error: Token not found in session store
Distinguishing Characteristics
| Condition | Behavior |
|---|---|
| Tab inactive < 10 min | Normal operation |
| Tab inactive > 10-15 min | 401 errors on message send |
| F5 / Page Refresh | Immediate resolution |
| Browser console clear | Same 401 error persists |
π§ Root Cause
Architectural Overview
The OpenClaw Control UI uses a dual-transport architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser Client β
β ββββββββββββββββ ββββββββββββββββββ βββββββββββββββ β
β β localStorage β β WebSocket Conn β β HTTP Client β β
β β (Auth Token) βββββΆβ (Message Bus) ββββββ (REST API) β β
β ββββββββββββββββ ββββββββββββββββββ βββββββββββββββ β
ββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
βββββββββΌββββββββ
β OpenClaw Core β
β (Gateway) β
ββββββββββββββββββ
Failure Sequence
- Initial Authentication: User authenticates; JWT stored in
localStorageasopenclaw_auth_token - Session Creation: Gateway creates server-side session mapped to WebSocket connection ID
- Active Period: Messages flow correctly via established WebSocket channel
- Timeout Trigger: After ~10-15 minutes, the server-side session expires due to inactivity timeout
- WebSocket Closure: Server closes WebSocket with code
1006or sendsSession expiredframe - Silent Reconnection: Client attempts reconnect but does NOT include auth token in reconnection handshake
- Auth Failure: New WebSocket connection is rejected with
401because session store is empty
Code Path Analysis
The bug resides in the client’s reconnection logic:
// Hypothetical problematic code in web-ui/src/services/connection.ts
class ConnectionManager {
async reconnect() {
// β BUG: Does not retrieve token from localStorage
const ws = new WebSocket(this.gatewayUrl);
ws.onopen = () => {
// Missing: this.authenticate();
};
}
// Proper implementation would include:
async authenticate() {
const token = localStorage.getItem('openclaw_auth_token');
this.ws.send(JSON.stringify({
type: 'AUTH',
token: token // β This step is missing in reconnect
}));
}
}
Session Management Discrepancy
The issue is compounded by a version mismatch:
CLI Version: 2026.2.23
Browser Version: 2026.2.17
This indicates the gateway may have been updated separately from the embedded web UI assets, potentially causing session validation logic to diverge between server and client.
Environment-Specific Factors
| Factor | Impact |
|---|---|
GATEWAY_SESSION_TIMEOUT | Default 600 seconds (10 min) |
GATEWAY_WEBSOCKET_PING_INTERVAL | May not be configured, causing TCP keepalive gaps |
| Browser Tab Backgrounding | Browsers may throttle WebSocket in background tabs |
| macOS Power Management | May suspend tab activity after display sleep |
π οΈ Step-by-Step Fix
Option 1: Client-Side Fix (Immediate - For Users)
Prerequisite: Browser DevTools access (F12)
- Open browser DevTools (F12)
- Navigate to Console tab
- Execute the following snippet before inactivity period:
// 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');
})();
Option 2: Server Configuration (For Administrators)
Step 1: Locate gateway configuration file:
# Linux/macOS
~/.openclaw/gateway.yaml
# Docker
docker exec openclaw-gateway cat /app/config/gateway.yaml
Step 2: Modify session timeout settings:
# Before (gateway.yaml)
server:
session_timeout: 600 # 10 minutes
# After (gateway.yaml)
server:
session_timeout: 28800 # 8 hours
websocket:
ping_interval: 30 # Send ping every 30 seconds
ping_timeout: 10 # Disconnect if no pong within 10 seconds
auth:
token_refresh_interval: 300 # Auto-refresh token every 5 minutes
Step 3: Restart the gateway service:
# Systemd
sudo systemctl restart openclaw-gateway
# Docker
docker restart openclaw-gateway
# Direct binary
./openclaw gateway restart
Option 3: Web UI Code Fix (For Developers)
File: web-ui/src/services/WebSocketManager.ts
// Before (broken reconnect logic)
class WebSocketManager {
private handleReconnect() {
this.socket = new WebSocket(this.url);
// Missing: Authentication on new connection
}
}
// After (corrected implementation)
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
}
});
}
}
}
Additional Fix for Session Store:
File: gateway/src/session/SessionStore.ts
// Before: Sessions expire on timeout alone
async createSession(token: string): Promise {
return this.sessions.create({
token,
expiresAt: Date.now() + SESSION_TIMEOUT
});
}
// After: Sessions can be extended via 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);
}
}
Option 4: Version Synchronization (For Version Mismatch Bug)
# Stop all OpenClaw services
openclaw stop --all
# Clear cached assets
rm -rf ~/.openclaw/cache/web-ui/
rm -rf ~/.openclaw/cache/assets/
# Reinstall to sync versions
openclaw update --force
# Restart services
openclaw start --all
π§ͺ Verification
Test Case 1: Basic Reconnection
# Terminal 1: Monitor gateway logs
openclaw logs --follow gateway
# Browser: Open DevTools Console and filter for "OpenClaw"
# Execute: Leave tab for exactly 12 minutes
# Expected: No 401 errors after reconnect
# Console should show:
# [OpenClaw] Connection lost
# [OpenClaw] Reconnecting...
# [OpenClaw] Auth sent
# [OpenClaw] Reconnected successfully
Test Case 2: WebSocket Ping/Pong Verification
# In browser console, verify ping/pong traffic
setInterval(() => {
console.table({
wsState: WebSocket.CONNECTING, // 0
wsOpen: WebSocket.OPEN, // 1
wsClosing: WebSocket.CLOSING, // 2
wsClosed: WebSocket.CLOSED // 3
});
}, 60000); // Check every minute
# Expected: State remains OPEN during backgrounding
Test Case 3: Session Persistence Check
# Check session TTL via gateway API
curl -s http://127.0.0.1:18789/api/v1/session/status \
-H "Authorization: Bearer $(cat ~/.openclaw/auth_token)" \
| jq .session
# Expected output:
# {
# "sessionId": "sess_abc123",
# "expiresAt": "2026-01-16T00:00:00Z",
# "ttl": 28800,
# "extendable": true
# }
Test Case 4: Load Test (Automated)
# Run the session timeout test suite
npm test -- --grep "session-timeout"
# Expected results:
# β reconnect-with-auth: No 401 after 15min inactivity
# β token-refresh: Token auto-renewed before expiry
# β multiple-reconnects: Auth preserved across 5 reconnect cycles
Verification Checklist
| Test | Command | Expected Result |
|---|---|---|
| Gateway accessible | curl http://127.0.0.1:18789/health | {“status”: “ok”} |
| WebSocket upgrade | websocat ws://127.0.0.1:18789/ws | Connection established |
| Auth token valid | Check localStorage.openclaw_auth_token | Non-empty JWT string |
| Version sync | openclaw version | Matches browser UI version |
β οΈ Common Pitfalls
Pitfall 1: Browser Privacy/Incognito Mode
Issue: localStorage is cleared when using Incognito/Private browsing in strict mode.
Symptom: Even after page refresh, authentication fails.
Workaround:
# Check if localStorage is accessible
console.log(localStorage.getItem('openclaw_auth_token'));
// If null in Incognito: Use persistent storage option in settings
Pitfall 2: Docker Network Segmentation
Issue: WebSocket connections from browser may route through Docker’s internal network differently than HTTP.
Symptom: HTTP 401 even with correct token; WebSocket upgrade fails.
Diagnosis:
# Check Docker port mappings
docker port openclaw-gateway
# Verify WebSocket endpoint is exposed
curl -I http://localhost:18789/ws
# Should return: 101 Switching Protocols
Fix:
# docker-compose.yaml addition
services:
gateway:
ports:
- "18789:18789" # HTTP/REST
- "18790:18790" # WebSocket (if separate)
Pitfall 3: macOS Battery/Energy Saver
Issue: macOS may throttle JavaScript execution in background tabs, preventing ping/pong keepalive.
Symptom: Sessions expire even with short timeout configured.
Workaround:
# Disable App Nap for browser
# Safari: Develop β Experimental Features β Disable Background Timer Throttling
# Chrome: Add --disable-background-timer-throttling flag
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-background-timer-throttling
Pitfall 4: Reverse Proxy Timeout
Issue: Nginx/Apache may close WebSocket connections due to proxy_read_timeout.
Symptom: 401 appears exactly at proxy timeout, not gateway timeout.
Fix:
# 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 hours
proxy_send_timeout 86400;
}
Pitfall 5: Multiple Tab Authentication
Issue: Opening multiple tabs creates multiple sessions; closing one may invalidate others if using single-session mode.
Symptom: 401 appears after opening a second tab and closing it.
Workaround: Enable multi-session mode in gateway configuration:
# gateway.yaml
auth:
allow_concurrent_sessions: true
session_mode: per-tab # Instead of per-user
Pitfall 6: Token Expiration vs. Session Expiration
Issue: JWT may expire independently of WebSocket session.
Symptom: 401 even immediately after authentication.
Diagnosis:
# Decode JWT to check expiration
atob(localStorage.openclaw_auth_token.split('.')[1])
# Look for "exp" claim - Unix timestamp
# Compare with current time
date +%s
π Related Errors
Related HTTP Errors
| Error Code | Issue | Connection |
|---|---|---|
401 Unauthorized | Invalid/expired authentication token | Direct descendant of this issue |
403 Forbidden | Valid token but insufficient permissions | Related auth flow |
407 Proxy Authentication Required | Proxy credentials needed | Different layer |
Related WebSocket Close Codes
| Close Code | Name | Description |
|---|---|---|
1000 | Normal Closure | Intentional disconnect |
1001 | Going Away | Server shutting down |
1006 | Abnormal Closure | Network failure or timeout (our case) |
1011 | Unexpected Error | Server-side error |
4001 | Authentication Failed | Custom gateway code |
Historical Issues in OpenClaw
- Issue #2847: "WebSocket disconnects randomly during long conversations" - Similar timeout mechanism
- Issue #2901: "Auth token not persisted across browser restart" - localStorage edge case
- Issue #3102: "Version mismatch between CLI and web UI" - Related to version discrepancy noted in this report
- Issue #3156: "Dashboard requires re-login every 5 minutes" - Shorter timeout variant
- Issue #3224: "WebSocket ping not working in background tabs" - macOS-specific
Related Documentation
- OpenClaw Authentication Architecture
- WebSocket Protocol Specification
- Session Management Configuration
- General Troubleshooting Guide
Known Affected Versions
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)