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 | |
} | |
} |
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-L37
So I think you are right, it won't cause infinite loops
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hmmm... I have to admit I am a bit rusty on this code and I may be wrong, but actually I don't think it will infinitely loop, because due to the return, the same request will not be retried. I have to admin I am using this code in production, but the actual request to get a new token does not use axios, so it may not be looping due to this.
Can you describe which flow could cause infinite loop? Do you mean if the tokenRefreshHandler uses axios to get a token an receives a 401 response?