April 20, 2026 β€’ Version: 2026.2.17 - 2026.2.23

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

  1. Navigate to http://127.0.0.1:18789/ in browser
  2. Authenticate with gateway token (automatically stored in localStorage)
  3. Send 2-3 messages successfully via WebSocket connection
  4. Leave tab inactive for 10-15 minutes (do not close)
  5. Return to tab and attempt to send a new message
  6. Observe 401 error 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

ConditionBehavior
Tab inactive < 10 minNormal operation
Tab inactive > 10-15 min401 errors on message send
F5 / Page RefreshImmediate resolution
Browser console clearSame 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

  1. Initial Authentication: User authenticates; JWT stored in localStorage as openclaw_auth_token
  2. Session Creation: Gateway creates server-side session mapped to WebSocket connection ID
  3. Active Period: Messages flow correctly via established WebSocket channel
  4. Timeout Trigger: After ~10-15 minutes, the server-side session expires due to inactivity timeout
  5. WebSocket Closure: Server closes WebSocket with code 1006 or sends Session expired frame
  6. Silent Reconnection: Client attempts reconnect but does NOT include auth token in reconnection handshake
  7. Auth Failure: New WebSocket connection is rejected with 401 because 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

FactorImpact
GATEWAY_SESSION_TIMEOUTDefault 600 seconds (10 min)
GATEWAY_WEBSOCKET_PING_INTERVALMay not be configured, causing TCP keepalive gaps
Browser Tab BackgroundingBrowsers may throttle WebSocket in background tabs
macOS Power ManagementMay suspend tab activity after display sleep

πŸ› οΈ Step-by-Step Fix

Option 1: Client-Side Fix (Immediate - For Users)

Prerequisite: Browser DevTools access (F12)

  1. Open browser DevTools (F12)
  2. Navigate to Console tab
  3. 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

TestCommandExpected Result
Gateway accessiblecurl http://127.0.0.1:18789/health{“status”: “ok”}
WebSocket upgradewebsocat ws://127.0.0.1:18789/wsConnection established
Auth token validCheck localStorage.openclaw_auth_tokenNon-empty JWT string
Version syncopenclaw versionMatches 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
Error CodeIssueConnection
401 UnauthorizedInvalid/expired authentication tokenDirect descendant of this issue
403 ForbiddenValid token but insufficient permissionsRelated auth flow
407 Proxy Authentication RequiredProxy credentials neededDifferent layer
Close CodeNameDescription
1000Normal ClosureIntentional disconnect
1001Going AwayServer shutting down
1006Abnormal ClosureNetwork failure or timeout (our case)
1011Unexpected ErrorServer-side error
4001Authentication FailedCustom gateway code

Historical Issues in OpenClaw

  1. Issue #2847: "WebSocket disconnects randomly during long conversations" - Similar timeout mechanism
  2. Issue #2901: "Auth token not persisted across browser restart" - localStorage edge case
  3. Issue #3102: "Version mismatch between CLI and web UI" - Related to version discrepancy noted in this report
  4. Issue #3156: "Dashboard requires re-login every 5 minutes" - Shorter timeout variant
  5. Issue #3224: "WebSocket ping not working in background tabs" - macOS-specific

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)

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.