mister.cake
mister.cake

Reputation: 165

Issue with cancelling axios .then logic with interceptors and cancel token

I set up an axios response interceptor for my react app. It works fine for catching most errors but one thing i am having trouble with is if the response is a 401 aka the user is not authed, the interceptor sends the user back to the login page. Now this works but the logic inside the .then from the original request still runs. This causes a type error as in the .then logic i am setting a state with the response data. Here is my current attempt at implementing a axios cancel token that is not working. See the code below. What am i missing here? What is the best way to achieve this with out having to add If/Else logic to every axios request to check if "data" is there or is the response is a 401, 200 ...?

AxiosInterceptor.js ...

export default withRouter({
    useSetupInterceptors: (history) => {
        axios.interceptors.response.use(response => {
            return response;
        }, error => {
            try {
                if (error.response.status === 401) {
                    history.push("/login");
                    Swal.fire({
                        title: '401 - Authorization Failed',
                        text: '',
                        icon: 'warning',
                        showCancelButton: false,
                        confirmButtonText: 'Close',
                    })
                    throw new axios.Cancel('Operation canceled');
                }
                return Promise.reject(error);
            } catch (error) {
                console.log(error)
            }
        });
    },
});

UserPage.js ...

function userPage() {
  var [pageData, setPageData] = useState('');
  var classes = useStyles();


  useEffect(() => {
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    const loadData = () => {
      try {
      axios.post('/api/getUserData', { cancelToken: source.token })
        .catch(function (error) {
          source.cancel();
        })
        .then(res => {
            const data = res.data;
            setPageData(data);
        })
      } catch (error) {
        if (axios.isCancel(error)) {
          console.log('Op Cancel')
        } else {
          throw error;
        }
      }
    };
      loadData();
    return () => {
      source.cancel();
    };
  }, []);

  return ( 
     ... 
  );
}
...

The error i get:

Unhandled Rejection (TypeError): Cannot read property 'data' of undefined

PROGRESS UPDATE:

I added some logic to my back-end that if the login is successful,

  1. i pass the expiration time of the JWT token back to my front end.

  2. Then push that expiration epoch to my redux store.

  3. On every request, in my 'AxiosInterceptor.js' file below, before returning a config back, i validate the exp value set in redux.

Now this works fine on initial login, but once the token has expired and you receive the popup from 'Swal.fire' and click 'return' it does two things:

  1. calls logOut action and returns all values to initial state. (This works fine. I validated with redux-devtools-extension)
  2. Now i can log back in. Everything starts to load fine but then i get the 'Swal.fire' dialog to return back to login page. When logging the user.exp and date.now to console i see some strange behavior(see comments):
// from redux-logger
action SET_EXP @ 20:05:42.721
redux-logger.js:1  prev state {user: {…}, _persist: {…}}
redux-logger.js:1  action     {type: "SET_EXP", payload: 1585267561036}

USEREXP 1585267561036 // this is the new EXP time set in redux, received from back end on login
AxiosInterceptors.js:17 Current Status =  1585267561036 false // first two axios calls on main page validate and indicate not expired
AxiosInterceptors.js:17 Current Status =  1585267561036 false
AxiosInterceptors.js:17 Current Status =  1585267495132 true // this is the value of the previos exp value that was set
AxiosInterceptors.js:17 Current Status =  1585267495132 true
AxiosInterceptors.js:17 Current Status =  1585267352424 true // this is the value that was set two login times ago
AxiosInterceptors.js:17 Current Status =  1585267352424 true

How is this possible? I verified with redux-devtools that once i am returned back to the login page, it is indeed empty. It appears the value in > redux-store is being rolled back to old values? I am using chrome Version 74.0.3729.131 (Official Build) (64-bit). I have tried with incognito mode and clearing cache and cookies.

New AxiosInterceptor.js ...

export default withRouter({
    useSetupInterceptors: (history) => {
    let user = useSelector(state => state.user) 
        axios.interceptors.request.use(config => {
         const { onLogo } = useLogout(history);
                console.log("Current Status = ", user.exp, Date.now() > user.exp)
                if (Date.now() > user.exp) {
                    Swal.fire({
                        title: '401 - Auth Failed',
                        text: '',
                        icon: 'warning',
                        showCancelButton: false,
                        confirmButtonText: 'Return',
                    }).then((result) => {
                        onLogo();
                    })
                    return {
                        ...config,
                        cancelToken: new CancelToken((cancel) => cancel('Cancel')) // Add cancel token to config to cancel request if redux-store expire value is exceeded
                      };
                } else {
                    return config;
                }
              }, error => { console.log(error)});

        axios.interceptors.response.use(response => {
            return response;
        }, error => {
            try {
            if (axios.isCancel(error)) { // check if canceled
                    return new Promise(() => {}); // return new promise to stop axios from proceeding to the .then
                }
                if (error.response.status === 401) {
                    history.push("/login");
                    Swal.fire({
                        title: '401 - Auth Failed',
                        text: '',
                        icon: 'warning',
                        showCancelButton: false,
                        confirmButtonText: 'Close',
                    })
                    throw new axios.Cancel('Operation canceled');
                }
                return Promise.reject(error);
            } catch (error) {
                console.log(error)
            }
        });
    },
});

function useLogo(history) {
    const dispatch = useDispatch()
    return {
        onLogo() {
            dispatch(allActs.userActs.logOut())
            history.push("/login");
        },
    }
}

Upvotes: 1

Views: 3738

Answers (1)

mister.cake
mister.cake

Reputation: 165

I tracked down the issue to the hook "useSelector" within react-redux. It seems this is some how returning cached data, after it already returned correct data. I am using version 7.2 at his time but i confirmed it also on v7.1. I have not tested on any other versions. I solved this by pulling the data from redux-persist Storage(localStorage) in the getExpire() function below. Not the most elegant solution but my application is now working as it should be.

export default withRouter({
    useSetupInterceptors: (history) => {
        const { onLogout } = useLogout(history);
        const CancelToken = axios.CancelToken;
        const { onExp } = useExp();

        axios.interceptors.request.use((config) => {
            const testexp = onExp();
            if (testexp) {
                Swal.fire({
                    title: '401 - Authorization Failed',
                    text: '',
                    icon: 'warning',
                    showCancelButton: false,
                    confirmButtonText: 'Return',
                }).then((result) => {
                    onLogout();

                })
                return {
                    ...config,
                    cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
                };
            } else {
                return config;
            }
        }, error => { console.log(error) });

        axios.interceptors.response.use(response => {
            return response;
        }, error => {
            try {
                if (axios.isCancel(error)) {
                    return new Promise(() => { });
                }
                return Promise.reject(error);
            } catch (error) {
                console.log(error)
            }
        });
    },
});

function getExpire () {
    var localStore = localStorage.getItem("persist:root")
    if (localStore) {
       let store = JSON.parse(localStore)
       return JSON.parse(store.exp)
    } 
    return 0

}

function useExp() {
   // const currentExp = useSelector(state => state.exp)
    return {
        onExp() {
            if (Date.now() > getExpire().exp) {
                return true
            } else { return false }
        },
    }
}

function useLogout(history) {
    const dispatch = useDispatch()
    return {
        onLogout() {
            dispatch(allActions.expAction.setLogout())
            history.push("/login");
        },
    }
}

Upvotes: 1

Related Questions