Last active
December 12, 2023 07:59
-
-
Save bragma/f68391596de71e1bfae066be80c259dc to your computer and use it in GitHub Desktop.
My take on 401/token refresh axios interceptor - use promises' implicit queue to retry all pending requests awaiting on a shared promise
This file contains 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 from 'axios' | |
export default class ApiClient { | |
constructor(baseUrl, tokenStorage) { | |
this.http = axios.create({ | |
baseURL: baseUrl | |
}) | |
this.tokenStorage = tokenStorage | |
this.setupTokenInterceptors() | |
} | |
setupTokenInterceptors() { | |
this.setTokenInterceptor = this.http.interceptors.request.use(request => { | |
let token = this.tokenStorage.getAccessToken() | |
if (token) { | |
request.headers['Authorization'] = `Bearer ${token}` | |
} | |
return request | |
}) | |
this.refreshTokenInterceptor = this.http.interceptors.response.use(undefined, async error => { | |
const response = error.response | |
if (response) { | |
if (response.status === 401 && error.config && !error.config.__isRetryRequest) { | |
try { | |
await this.tokenStorage.refreshAccessToken() | |
} catch (authError) { | |
// refreshing has failed, but report the original error, i.e. 401 | |
return Promise.reject(error) | |
} | |
// retry the original request | |
error.config.__isRetryRequest = true | |
return this.http(error.config) | |
} | |
} | |
return Promise.reject(error) | |
}) | |
} | |
} |
This file contains 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
export default class TokenService { | |
constructor(baseKey) { | |
this.accessTokenKey = `${baseKey}-access-token` | |
this.getAccessToken = this.getAccessToken.bind(this) | |
this.setAccessToken = this.setAccessToken.bind(this) | |
this.removeAccessToken = this.removeAccessToken.bind(this) | |
this.tokenRefreshHandler = null | |
this.refreshing = null | |
this.refreshAccessToken = this.refreshAccessToken.bind(this) | |
} | |
getAccessToken() { | |
return localStorage.getItem(this.accessTokenKey) | |
} | |
setAccessToken(accessToken) { | |
localStorage.setItem(this.accessTokenKey, accessToken) | |
} | |
removeAccessToken() { | |
localStorage.removeItem(this.accessTokenKey) | |
} | |
refreshAccessToken() { | |
if (!this.tokenRefreshHandler) throw new Error('No token refresh handler installed') | |
if (!this.refreshing) { | |
this.refreshing = this.tokenRefreshHandler() | |
this.refreshing.then(() => { | |
this.refreshing = null | |
}) | |
} | |
return this.refreshing | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do you mean if the tokenRefreshHandler uses axios to get a token an receives a 401 response?
Yes, that is the case I had in mind, but looking again at the snippet, I think that token refresh request would fall here https://gist.github.com/bragma/f68391596de71e1bfae066be80c259dc#file-apiclient-js-L37So I think you are right, it won't cause infinite loops