How to pause and restart API calls in react-redux app while access token is being refreshed?

Viewed 721

We have a react-redux app that fetches data using multiple API calls with every page load. The app follows the OAuth2 protocol. It has an access token that expires frequently and a refresh token to be used to get a new access token. If an API call is made with an expired access token, a 401 error is received with error message "API token is expired." Then we need to get a new token from the auth server.

My problem is this: When a page loads, say 8 API calls were dispatched. We receive 3 successful 200s but from the 4th response onwards, we receive 401 "API token is expired." At that point, I want to put all API calls that I have already made but didn't receive a response or received 401 error in a queue until I refresh the access token. After the access token is successfully refreshed, I want to re-do the API calls saved in the queue. How can I achieve this?

Looking for this online, I found that redux-saga might work, but didn't see any indication that it can be used for this use case.

1 Answers

I also used to handle this case. This is my solution:

/**
 * Connect to API
 */
export const makeRequest = (args) => {
    const request = fetch(args)//example

    return _retryRequestIfExpired(request, args)
}


/**
 * Fake get access token.
 */
const _getNewAccessToken = () => {
    return new Promise((resolve, reject) => {
        resolve('xyz')
    })
}

const _retryRequestIfExpired = (request, args) => {
    return request.catch(error => {
        if (error === 'abc') {//Any reason you want
            return _refreshAccessToken()
                .then(newAccessToken => {
                    const updateArgs = {
                        ...args,
                        headers: {
                            'Authorization': newAccessToken
                        }
                    }

                    //Retry with new access token
                    return makeRequest(updateArgs)
                })
        }

        throw error
    })
}

/**
 * Important
 */
let _isRefreshingToken = false
let _subscribers = []

const _subscribe = subscriber => {
    if (typeof subscriber !== 'function' || _subscribers.indexOf(subscriber) !== -1) {
        return false
    }

    _subscribers = [].concat(_subscribers, [subscriber])
}

const _broadcast = (error = null, data) => {
    _isRefreshingToken = false

    _subscribers.forEach(subscriber => {
        subscriber(error, data)
    })

    _subscribers = []
}

const _refreshAccessToken = () => {
    if (_isRefreshingToken) {//If a request is creating new access token
        return new Promise((resolve, reject) => {
            const subscriber = (error, accessToken) => {
                if (error) {
                    return reject(error)
                }

                return resolve(accessToken)
            }

            _subscribe(subscriber)
        })
    }

    _isRefreshingToken = true

    return _getNewAccessToken()
        .then(accessToken => {
            _broadcast(null, accessToken)

            return accessToken
        })
        .catch(error => {
            _broadcast(error)

            throw error
        })
}
/**
 * End Important
 */

In this way, only the first request will actually create a new access token and remaining requests will temporarily be stopped until a new access token is created.

Related