Heuristic:Langgenius Dify Token Refresh Loop Prevention
| Knowledge Sources | |
|---|---|
| Domains | Frontend, Authentication |
| Last Updated | 2026-02-08 11:00 GMT |
Overview
Authentication token refresh pattern that uses raw `globalThis.fetch()` instead of `baseFetch()` and cross-tab localStorage coordination to prevent infinite 401 loops and duplicate refresh requests.
Description
When an API request receives a 401 response, the frontend automatically attempts to refresh the authentication token. If the refresh endpoint itself also returns 401 and uses the same `baseFetch()` function (which intercepts 401 responses), it creates an infinite loop of refresh attempts. Additionally, in multi-tab browser scenarios, multiple tabs may simultaneously attempt to refresh the same token, causing race conditions and wasted requests.
Usage
Apply this heuristic whenever you work on authentication infrastructure in the frontend, specifically the HTTP client layer (`web/service/refresh-token.ts` and `web/service/fetch.ts`). This protects all API calls made by all implementations.
The Insight (Rule of Thumb)
- Action 1: Use `globalThis.fetch()` (not `baseFetch()`) for the token refresh request to avoid recursive 401 interception.
- Action 2: Use `localStorage` with a `is_other_tab_refreshing` key to coordinate refresh across browser tabs.
- Action 3: Always clear the localStorage lock on `beforeunload` to prevent permanent cross-tab deadlocks.
- Value: Prevents cascading authentication failures that could render the entire application unusable.
- Trade-off: 1-second polling interval during cross-tab wait adds slight latency to token recovery.
Reasoning
The `baseFetch()` function includes a 401 interceptor that triggers token refresh. If this same function is used to perform the refresh, a 401 from the refresh endpoint triggers another refresh, creating an infinite loop that exhausts browser resources and generates hundreds of failed requests per second. Using raw `fetch()` bypasses this interceptor entirely.
The localStorage coordination prevents a scenario where 5 open tabs each independently detect a 401, each sends a refresh request, and 4 of those fail because the token was already rotated by the first tab. Instead, only one tab refreshes while others poll localStorage waiting for completion.
Code Evidence
From `web/service/refresh-token.ts:43-47`:
// Do not use baseFetch to refresh tokens.
// If a 401 response occurs and baseFetch itself attempts to refresh the token,
// it can lead to an infinite loop if the refresh attempt also returns 401.
const [error, ret] = await fetchWithRetry(globalThis.fetch(
`${API_PREFIX}/refresh-token`, { method: 'POST', credentials: 'include' }
))
From `web/service/refresh-token.ts:4-21`:
const LOCAL_STORAGE_KEY = 'is_other_tab_refreshing'
function waitUntilTokenRefreshed() {
return new Promise((resolve) => {
const interval = setInterval(() => {
if (localStorage.getItem(LOCAL_STORAGE_KEY) !== '1') {
clearInterval(interval)
resolve(undefined)
}
}, 1000)
})
}
// Release lock on page unload to prevent permanent deadlocks
window.addEventListener('beforeunload', () => {
localStorage.removeItem(LOCAL_STORAGE_KEY)
})