useStream hook: stale client closure causes 403 errors after token refresh #393

Open
opened 2026-02-15 18:16:24 -05:00 by yindo · 1 comment
Owner

Originally created by @EUN-GYUL on GitHub (Jan 5, 2026).

Bug Description

The useStream hook in @langchain/langgraph-sdk/react uses a stale client reference after the apiKey prop changes. This causes HTTP 403 authentication errors when the JWT token is refreshed while the component is mounted.

Root Cause

In useStream.ts, the useThreadHistory callback captures the initial client object in a closure:

// From node_modules/@langchain/langgraph-sdk/dist/react/useStream.js
const useThreadHistory = useCallback(
  async (threadId, options) => {
    // Uses `client` from closure - never updates when apiKey changes
    const history = await client.threads.getHistory(threadId, options);
    // ...
  },
  [] // Empty dependency array - closure is never refreshed
);

The client is created from apiKey:

const client = useMemo(() => {
  if (!apiUrl) return null;
  return new Client({ apiUrl, apiKey });
}, [apiUrl, apiKey]);

While client updates when apiKey changes, useThreadHistory still holds the old client reference due to the empty dependency array.

Reproduction Steps

  1. Mount a component using useStream with an initial JWT token
  2. Token expires/refreshes, parent component passes new apiKey prop
  3. User sends a message via submit()
  4. Expected: Request uses new token
  5. Actual: Request uses old token → 403 error

Minimal Reproduction

function ChatComponent({ token }) {
  const stream = useStream({
    apiUrl: "https://api.example.com",
    apiKey: token, // Changes when refreshed
    assistantId: "my-assistant",
    threadId: null,
  });

  // After token refresh, submit() still uses old token
  return <button onClick={() => stream.submit(...)}>Send</button>;
}

Current Workaround

Force component remount when token changes using React key:

<ChatComponent key={token || "no-token"} token={token} />

This destroys and recreates the entire component, creating a new client instance.

Proposed Fix

Add client to the dependency array of useThreadHistory and other callbacks:

const useThreadHistory = useCallback(
  async (threadId, options) => {
    const history = await client.threads.getHistory(threadId, options);
    // ...
  },
  [client] // Add client dependency
);

Environment

  • @langchain/langgraph-sdk: ^0.0.41
  • React: 19.x
  • Next.js: 15.x

Related Issues

  • #4825 - Stale History in useStream
  • #1295 - useStream Hook Not Updating

Impact

  • Prevents seamless token refresh in production apps
  • Forces unnecessary component remounts as workaround
  • Affects any app using JWT authentication with token rotation
Originally created by @EUN-GYUL on GitHub (Jan 5, 2026). ## Bug Description The `useStream` hook in `@langchain/langgraph-sdk/react` uses a stale `client` reference after the `apiKey` prop changes. This causes HTTP 403 authentication errors when the JWT token is refreshed while the component is mounted. ## Root Cause In `useStream.ts`, the `useThreadHistory` callback captures the initial `client` object in a closure: ```typescript // From node_modules/@langchain/langgraph-sdk/dist/react/useStream.js const useThreadHistory = useCallback( async (threadId, options) => { // Uses `client` from closure - never updates when apiKey changes const history = await client.threads.getHistory(threadId, options); // ... }, [] // Empty dependency array - closure is never refreshed ); ``` The `client` is created from `apiKey`: ```typescript const client = useMemo(() => { if (!apiUrl) return null; return new Client({ apiUrl, apiKey }); }, [apiUrl, apiKey]); ``` While `client` updates when `apiKey` changes, `useThreadHistory` still holds the old `client` reference due to the empty dependency array. ## Reproduction Steps 1. Mount a component using `useStream` with an initial JWT token 2. Token expires/refreshes, parent component passes new `apiKey` prop 3. User sends a message via `submit()` 4. **Expected**: Request uses new token 5. **Actual**: Request uses old token → 403 error ## Minimal Reproduction ```tsx function ChatComponent({ token }) { const stream = useStream({ apiUrl: "https://api.example.com", apiKey: token, // Changes when refreshed assistantId: "my-assistant", threadId: null, }); // After token refresh, submit() still uses old token return <button onClick={() => stream.submit(...)}>Send</button>; } ``` ## Current Workaround Force component remount when token changes using React `key`: ```tsx <ChatComponent key={token || "no-token"} token={token} /> ``` This destroys and recreates the entire component, creating a new `client` instance. ## Proposed Fix Add `client` to the dependency array of `useThreadHistory` and other callbacks: ```typescript const useThreadHistory = useCallback( async (threadId, options) => { const history = await client.threads.getHistory(threadId, options); // ... }, [client] // Add client dependency ); ``` ## Environment - `@langchain/langgraph-sdk`: ^0.0.41 - React: 19.x - Next.js: 15.x ## Related Issues - #4825 - Stale History in useStream - #1295 - useStream Hook Not Updating ## Impact - Prevents seamless token refresh in production apps - Forces unnecessary component remounts as workaround - Affects any app using JWT authentication with token rotation
Author
Owner

@sivashankar0398-cmyk commented on GitHub (Jan 14, 2026):

Hi! I’d like to work on this issue by adding client to the dependency array of useThreadHistory to avoid stale closures. I’ll also include a regression test to ensure correct behavior after token refresh. Please let me know if this approach sounds good.

@sivashankar0398-cmyk commented on GitHub (Jan 14, 2026): Hi! I’d like to work on this issue by adding client to the dependency array of useThreadHistory to avoid stale closures. I’ll also include a regression test to ensure correct behavior after token refresh. Please let me know if this approach sounds good.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#393