使用拦截器和取消令牌取消 axios .then 逻辑的问题

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

我为我的 React 应用程序设置了一个 axios 响应拦截器。它可以很好地捕获大多数错误,但我遇到的一件事是如果响应是 401 也就是用户未授权,拦截器会将用户发送回登录页面。现在这有效,但原始请求中的 .then 中的逻辑仍在运行。这会导致类型错误,如我正在使用响应数据设置状态的 .then 逻辑。这是我目前尝试实现一个不起作用的 axios 取消令牌。请参阅下面的代码。我在这里错过了什么?无需向每个 axios 请求添加 If/Else 逻辑来检查 "data" 是否存在或响应是否为 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 ( 
     ... 
  );
}
...

我得到的错误:

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

进度更新:

我在后端添加了一些逻辑,如果登录成功,

  1. 我将 JWT 令牌的过期时间传回我的前端。

  2. 然后将该过期时间推送到我的 redux 存储。

  3. 根据每个请求,在我下面的 'AxiosInterceptor.js' 文件中,在 return 返回配置之前,我验证 redux 中设置的 exp 值。

现在这在初始登录时工作正常,但是一旦令牌过期并且您收到来自 'Swal.fire' 的弹出窗口并单击 'return' 它会做两件事:

  1. 调用 logOut 操作并 return 将所有值设置为初始状态。 (这很好用。我用 redux-devtools-extension 验证了)
  2. 现在我可以重新登录了。一切都开始正常加载,但随后我得到 'Swal.fire' 对话框 return 回到登录页面。当将 user.exp 和 date.now 记录到控制台时,我看到一些奇怪的行为(见评论):
// 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.

新建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");
        },
    }
}

我在 react-redux 中找到了钩子 "useSelector" 的问题。在它已经返回正确的数据之后,这似乎是返回缓存数据的一些方式。我当时使用的是 7.2 版,但我也在 v7.1 上确认了它。我没有在任何其他版本上测试过。我通过在下面的 getExpire() 函数中从 redux-persist Storage(localStorage) 中提取数据来解决这个问题。这不是最优雅的解决方案,但我的应用程序现在可以正常工作了。

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");
        },
    }
}