Skip to content

Instantly share code, notes, and snippets.

@bragma
Last active December 12, 2023 07:59
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bragma/f68391596de71e1bfae066be80c259dc to your computer and use it in GitHub Desktop.
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
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)
})
}
}
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
}
}
@luispeerez
Copy link

Don't yo think this rejection https://gist.github.com/bragma/f68391596de71e1bfae066be80c259dc#file-apiclient-js-L33 should be marked with the __isRetryRequest flag, in order to avoid inifnite loops when refresh fails?

@bragma
Copy link
Author

bragma commented Sep 30, 2019

Don't yo think this rejection https://gist.github.com/bragma/f68391596de71e1bfae066be80c259dc#file-apiclient-js-L33 should be marked with the __isRetryRequest flag, in order to avoid inifnite loops when refresh fails?

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?

@luispeerez
Copy link

luispeerez commented Sep 30, 2019

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