Skip to content

fix: chat WebSocket reconnect reuses a stale token after refresh - #1016

Open
kuhlsnu wants to merge 2 commits into
siteboon:mainfrom
kuhlsnu:fix/ws-reconnect-stale-token
Open

fix: chat WebSocket reconnect reuses a stale token after refresh#1016
kuhlsnu wants to merge 2 commits into
siteboon:mainfrom
kuhlsnu:fix/ws-reconnect-stale-token

Conversation

@kuhlsnu

@kuhlsnu kuhlsnu commented Jul 13, 2026

Copy link
Copy Markdown

authenticatedFetch (src/utils/api.js) persists a refreshed JWT to localStorage['auth-token'] when the server returns X-Refreshed-Token. But WebSocketContext builds the chat WS URL from AuthContext's in-memory token, which is seeded once at mount and only updated by login/logout, never re-synced from localStorage.

As a result, on a long-lived tab the chat WS auto-reconnect keeps reusing the original in-memory token and eventually fails at that token's true expiry, even though REST calls keep silently refreshing. The shell WS (src/components/shell/utils/socket.ts) and file upload already read localStorage fresh and are unaffected.

This reads the freshest auth-token from localStorage at (re)connect (falling back to the in-memory token), matching the shell-WS and file-upload paths.

One-line frontend change, no new dependencies. I didn't add a test since the repo has no frontend test harness, but I have a regression test for this from a downstream fork and am happy to contribute it if you add one.

Summary by CodeRabbit

  • Bug Fixes
    • Improved WebSocket connection reliability by using the most up-to-date authentication token available in the browser when establishing a connection.
    • Preserved existing WebSocket connection, messaging, and error-handling behavior.

authenticatedFetch persists a refreshed JWT to localStorage on X-Refreshed-Token,
but WebSocketContext builds the chat WS URL from AuthContext in-memory token,
which is never re-synced. On a long-lived tab the chat WS reconnect reuses the stale
token and eventually fails at the token true expiry while REST keeps refreshing.
Read the freshest auth-token from localStorage at (re)connect, matching the shell-WS
and file-upload paths.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7929de76-8487-4595-a12e-d18a313bb0ee

📥 Commits

Reviewing files that changed from the base of the PR and between 8b5bb22 and c71dd00.

📒 Files selected for processing (1)
  • src/contexts/WebSocketContext.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/contexts/WebSocketContext.tsx

📝 Walkthrough

Walkthrough

The WebSocket connection flow now reads the latest authentication token from localStorage when available, falling back to the in-memory token before building the WebSocket URL. Connection lifecycle handlers are unchanged.

Changes

WebSocket authentication

Layer / File(s) Summary
Token selection and WebSocket URL construction
src/contexts/WebSocketContext.tsx
The connect callback prefers the refreshed localStorage token and passes it to buildWebSocketUrl, with the in-memory token as fallback.

Poem

A bunny found a token bright,
And tucked it in the socket’s flight.
Fresh from storage, off it goes,
Past connection’s waiting pose.
Hop, hop—authentication grows!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: WebSocket reconnects now use the latest token instead of a stale one after refresh.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/contexts/WebSocketContext.tsx (1)

108-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared storage-key constant.

src/components/auth/constants.ts:1 already defines AUTH_TOKEN_STORAGE_KEY; use it here instead of duplicating 'auth-token' to prevent silent contract drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/WebSocketContext.tsx` around lines 108 - 112, Update the
freshToken lookup in the WebSocket reconnect logic to use the shared
AUTH_TOKEN_STORAGE_KEY constant from the auth constants module instead of the
duplicated 'auth-token' literal, while preserving the existing localStorage
fallback to token.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/contexts/WebSocketContext.tsx`:
- Around line 108-113: Update the token lookup in connect() to use
AUTH_TOKEN_STORAGE_KEY and isolate localStorage.getItem() in a narrow try/catch.
If storage is unavailable or the read throws, retain the in-memory token
fallback so reconnect proceeds; keep buildWebSocketUrl using the resolved token.

---

Nitpick comments:
In `@src/contexts/WebSocketContext.tsx`:
- Around line 108-112: Update the freshToken lookup in the WebSocket reconnect
logic to use the shared AUTH_TOKEN_STORAGE_KEY constant from the auth constants
module instead of the duplicated 'auth-token' literal, while preserving the
existing localStorage fallback to token.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 972d3501-ebd0-4aea-b175-444c7f996e44

📥 Commits

Reviewing files that changed from the base of the PR and between 038d960 and 8b5bb22.

📒 Files selected for processing (1)
  • src/contexts/WebSocketContext.tsx

Comment on lines +108 to +113
// Read the freshest token from localStorage (authenticatedFetch updates it
// on an X-Refreshed-Token) so a reconnect uses the refreshed token rather
// than a stale in-memory one. Mirrors the shell-WS / file-upload paths.
const freshToken =
(typeof localStorage !== 'undefined' && localStorage.getItem('auth-token')) || token;
const wsUrl = buildWebSocketUrl(freshToken);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== File map ==\n'
git ls-files 'src/contexts/WebSocketContext.tsx' 'src/components/auth/constants.ts'

printf '\n== WebSocketContext outline ==\n'
ast-grep outline src/contexts/WebSocketContext.tsx --view expanded || true

printf '\n== constants outline ==\n'
ast-grep outline src/components/auth/constants.ts --view expanded || true

printf '\n== WebSocketContext relevant lines ==\n'
nl -ba src/contexts/WebSocketContext.tsx | sed -n '70,150p'

printf '\n== Auth token constant file ==\n'
nl -ba src/components/auth/constants.ts | sed -n '1,120p'

printf '\n== Search for auth-token key usage ==\n'
rg -n "auth-token|AUTH_TOKEN_STORAGE_KEY" src || true

Repository: siteboon/claudecodeui

Length of output: 990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== WebSocketContext excerpt ==\n'
sed -n '90,130p' src/contexts/WebSocketContext.tsx | cat -n

printf '\n== Auth token constant ==\n'
sed -n '1,20p' src/components/auth/constants.ts | cat -n

printf '\n== Exact usages ==\n'
rg -n "AUTH_TOKEN_STORAGE_KEY|auth-token|localStorage\.getItem|try \{|catch" src/contexts/WebSocketContext.tsx src/components/auth/constants.ts

Repository: siteboon/claudecodeui

Length of output: 3064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== WebSocketContext 105-170 ==\n'
sed -n '105,170p' src/contexts/WebSocketContext.tsx | cat -n

printf '\n== Any storage/error handling nearby ==\n'
rg -n "localStorage|AUTH_TOKEN_STORAGE_KEY|catch \\(error\\)|setTimeout|reconnect" src/contexts/WebSocketContext.tsx src/components/auth/constants.ts

Repository: siteboon/claudecodeui

Length of output: 4930


Preserve the fallback when storage access fails.

localStorage.getItem() can throw when browser storage is unavailable, and the outer try aborts connect() before the in-memory token fallback runs. Wrap only the storage read so reconnects still proceed with token, and use AUTH_TOKEN_STORAGE_KEY instead of the duplicated 'auth-token' literal.

Proposed fix
-      const freshToken =
-        (typeof localStorage !== 'undefined' && localStorage.getItem('auth-token')) || token;
+      let freshToken = token;
+      try {
+        const storedToken =
+          typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) : null;
+        freshToken = storedToken || token;
+      } catch {
+        // Use the in-memory token when storage is unavailable.
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Read the freshest token from localStorage (authenticatedFetch updates it
// on an X-Refreshed-Token) so a reconnect uses the refreshed token rather
// than a stale in-memory one. Mirrors the shell-WS / file-upload paths.
const freshToken =
(typeof localStorage !== 'undefined' && localStorage.getItem('auth-token')) || token;
const wsUrl = buildWebSocketUrl(freshToken);
// Read the freshest token from localStorage (authenticatedFetch updates it
// on an X-Refreshed-Token) so a reconnect uses the refreshed token rather
// than a stale in-memory one. Mirrors the shell-WS / file-upload paths.
let freshToken = token;
try {
const storedToken =
typeof localStorage !== 'undefined' ? localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) : null;
freshToken = storedToken || token;
} catch {
// Use the in-memory token when storage is unavailable.
}
const wsUrl = buildWebSocketUrl(freshToken);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/WebSocketContext.tsx` around lines 108 - 113, Update the token
lookup in connect() to use AUTH_TOKEN_STORAGE_KEY and isolate
localStorage.getItem() in a narrow try/catch. If storage is unavailable or the
read throws, retain the in-memory token fallback so reconnect proceeds; keep
buildWebSocketUrl using the resolved token.

Replace the duplicated 'auth-token' string literal with the shared
AUTH_TOKEN_STORAGE_KEY constant from components/auth/constants to prevent
silent storage-key contract drift. Addresses CodeRabbit review on siteboon#1016.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant