Last active
April 10, 2025 06:02
-
-
Save hoangtrung99/535cff27378fc4c36cd7f48d386d1bf1 to your computer and use it in GitHub Desktop.
axios instance handle refresh token
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios, { type AxiosError, type InternalAxiosRequestConfig, type AxiosResponse } from 'axios' | |
/** | |
* Create axios instance with default configuration | |
*/ | |
const request = axios.create({ | |
baseURL: import.meta.env.VITE_API_URL, | |
timeout: 5000 | |
}) | |
/** | |
* Get access token from store or session | |
* @returns access token or null if not found | |
*/ | |
const getToken = async () => { | |
// TODO: Get token from your store or local storage | |
return 'token' | |
} | |
/** | |
* Get new access token using refresh token | |
* @returns new access token | |
*/ | |
const getNewAccessToken = async () => { | |
// TODO: Implement refresh token logic | |
// Example: Call refresh token API or use your auth library | |
return 'new_token' | |
} | |
/** | |
* Interface for Promises in the queue during token refresh | |
*/ | |
interface QueueItem { | |
resolve: (token: string) => void | |
reject: (error: Error) => void | |
} | |
// Token refresh state | |
let isRefreshing = false | |
let pendingRequests: QueueItem[] = [] | |
/** | |
* Process pending requests after token refresh | |
* @param error Error if token refresh failed | |
* @param token New token if refresh succeeded | |
*/ | |
const processPendingRequests = (error: Error | null, token: string | null = null) => { | |
pendingRequests.forEach(request => { | |
if (error) { | |
request.reject(error) | |
} else if (token) { | |
request.resolve(token) | |
} | |
}) | |
// Clear all pending requests after processing | |
pendingRequests = [] | |
} | |
/** | |
* Register a request to wait for a new token | |
* This function creates a Promise that will be resolved when a new token is available | |
* or rejected if token refresh fails | |
* @returns Promise that resolves with the new token | |
*/ | |
const waitForNewToken = (): Promise<string> => { | |
return new Promise<string>((resolve, reject) => { | |
// Add callbacks to the pending queue | |
// These will be called by processPendingRequests when token refresh completes | |
pendingRequests.push({ | |
// Will be called with the new token when refresh succeeds | |
resolve: (token: string) => resolve(token), | |
// Will be called with the error when refresh fails | |
reject: (error: Error) => reject(error) | |
}) | |
}) | |
} | |
/** | |
* Retry a failed request with a new token | |
* @param originalConfig The original request configuration | |
* @param token The new access token | |
* @returns A Promise with the retry request | |
*/ | |
const retryRequestWithToken = ( | |
originalConfig: CustomInternalAxiosRequestConfig, | |
token: string | |
): Promise<AxiosResponse> => { | |
if (originalConfig.headers) { | |
originalConfig.headers.Authorization = `Bearer ${token}` | |
} | |
return request(originalConfig) | |
} | |
/** | |
* Interceptor to add token to header of each request | |
*/ | |
request.interceptors.request.use(async (config) => { | |
const token = await getToken() | |
if (token) { | |
config.headers.Authorization = `Bearer ${token}` | |
} | |
return config | |
}) | |
/** | |
* Extend axios config to add _retry field | |
*/ | |
interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig { | |
_retry?: boolean | |
} | |
/** | |
* Response interceptor, including token refresh handling when receiving 401 error | |
*/ | |
request.interceptors.response.use( | |
// Return normal response if no error | |
(response) => response, | |
// Error handling | |
async (error: AxiosError) => { | |
const originalRequest = error.config as | |
| CustomInternalAxiosRequestConfig | |
| undefined | |
// Check if 401 (Unauthorized) error and request can be retried | |
const isUnauthorizedError = error.response?.status === 401 | |
const canRetryRequest = originalRequest && !originalRequest._retry | |
if (isUnauthorizedError && canRetryRequest) { | |
// If already refreshing token (another request is handling it) | |
if (isRefreshing) { | |
try { | |
// Wait for the active refresh process to complete | |
// This Promise will be resolved when processPendingRequests is called | |
const newToken = await waitForNewToken() | |
// When we get here, the token has been refreshed by another request | |
// Now we can retry our original request with the new token | |
return retryRequestWithToken(originalRequest, newToken) | |
} catch (err) { | |
// Token refresh failed, propagate the error | |
return Promise.reject(err) | |
} | |
} | |
// Mark this request as retried and start refresh process | |
originalRequest._retry = true | |
isRefreshing = true | |
try { | |
// TODO: Get new token using refresh token | |
const newToken = await getNewAccessToken() | |
if (!newToken) { | |
throw new Error('Failed to refresh token') | |
} | |
// TODO: Save new token to your store | |
// Example: saveTokenToStore(newToken) | |
// Process all pending requests with new token | |
// This will resolve all the waitForNewToken() promises with the new token | |
processPendingRequests(null, newToken) | |
// Retry original request with new token | |
return retryRequestWithToken(originalRequest, newToken) | |
} catch (refreshError) { | |
// Convert error if needed | |
const error = | |
refreshError instanceof Error | |
? refreshError | |
: new Error('Unknown error when refreshing token') | |
// Notify all pending requests of the error | |
// This will reject all the waitForNewToken() promises with the error | |
processPendingRequests(error, null) | |
// TODO: Handle logout | |
// Example: | |
// setAuthState({ isAuthenticated: false }) | |
// await signOut() | |
return Promise.reject(refreshError) | |
} finally { | |
// Mark refresh process as completed | |
isRefreshing = false | |
} | |
} | |
// Return original error if not 401 or can't retry | |
return Promise.reject(error) | |
} | |
) | |
export default request |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment