Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Where is the correct place to put interceptors in React app? All calls get blocked after navigation #2315

Closed
ilyador opened this issue Jul 27, 2019 · 13 comments

Comments

@ilyador
Copy link

ilyador commented Jul 27, 2019

Right now I have axios.interceptors.request.use in the Router component. I use it to make sure all requests have an access token. I put it there so I can access the React Context and save user data and user-related functions there.

However, I am having a weird bug that when I logout and then login again (without refreshing the page) the interceptor can't make any calls after the login.

axios.interceptors.request.use( async(config) => {
  console.log(config)  // undefined

and I'm getting this error:

TypeError: Cannot read property 'cancelToken' of undefined
    at throwIfCancellationRequested (dispatchRequest.js:20)
    at dispatchRequest (dispatchRequest.js:33)

I tried putting it in the topmost component and still getting the same issue that after a redirect from login the calls are blocked again.

This is the entire component.
The navigation inside the inner routes works fine, the only problem is navigating on and out of login page

const Routes = () => {
  const { user, refreshAccessToken } = useContext(UserContext)

  axios.interceptors.request.use( async(config) => {
    let accessToken = getAccessToken()
    let serverCallUrl = new URL(config.url)

    if (serverCallUrl.pathname.includes('/auth')) return config

    if (accessToken && user) {
      const { exp } = jwtDecode(accessToken)

      if (Date.now() > exp * 1000) {
        await refreshAccessToken()
        accessToken = getAccessToken()
      }

      config.headers['authorization'] = `Bearer ${accessToken}`
      config.headers['cache-control'] = `no-cache`

      return config
    }

  }, function (error) {
    return Promise.reject(error)
  })


  const PrivateRoute = ({ component: Component, ...rest }) => (
    <Route {...rest} render={
      props => user
        ? <HomePage renderProps={props}><Component/></HomePage>
        : <Redirect to={'/login'}/>
    }/>
  )


  return (
    <Router>
      <Switch>
        <Route
          exact path={'/'}
          render={() => <Redirect to={'/aaa'}/>}
        />
        <Route
          path={'/login'}
          render={() => user
            ? <Redirect to={'/aaa'}/>
            : <LogIn/>
          }
        />
        <PrivateRoute path='/aaa' component={Comp1}/>
        <PrivateRoute path='/bbb' component={Comp2}/>
        <PrivateRoute path='/ccc' component={Comp3}/>
      </Switch>
    </Router>
  )
}
@crhistianramirez
Copy link
Contributor

crhistianramirez commented Jul 28, 2019

Not an axios expert by any means but I ran into this while searching for my own question in issues and thought I'd share what we're doing.

Can you initialize at the entry point, where React is getting bootstrapped? This is where we're doing it:

ConfigureErrorHandler(); // has the logic to set up interceptor

ReactDOM.render(
  <Provider store={store}>
    <MuiThemeProvider theme={ORDERCLOUD_THEME}>
      <AlertContainer />
      <App />
    </MuiThemeProvider>
  </Provider>,
  document.getElementById('root') as HTMLElement
);

@ilyador
Copy link
Author

ilyador commented Jul 28, 2019

@crhistianramirez Thanx, but the issue is that I want (if possible) to have the interceptor inside a component, so I will have access to the React state and Context.

@JaminFarr
Copy link

I think a couple of things might be happening.

Multiple interceptors can be added to an Axios instance. In this case a new interceptor is added every time the Route component is rendered. This will happen on every route change creating an ever growing list of callbacks.

The useEffect hook can help here

// Remove the existing interceptor when either the user or refreshAccessToken change.
useEffect(() => {
    const authInterceptor = axios.interceptors.request.use(function () {/*...*/});
    return () => {
        axios.interceptors.request.eject(authInterceptor);
    }
}, [user, refreshAccessToken])

I think the undefined error is happening because the interceptor doesn't always return the config. If either the user or refreshAccessToken are undefined the function doesn't return anything. Axios is expecting a config object and looking for all the options, such as cancelToken, on the returned undefined value.

Moving the return config out of the if should fix this or throwing if you don't want the request to happen.

I think the reason console.log is logging undefined is because there are multiple interceptors and an earlier interceptor has returned undefined.

@ilyador
Copy link
Author

ilyador commented Jul 30, 2019

@JaminFarr I tried removing all the ifs and it was still returning undefined.

If there were multiple interceptors wouldn't at least one of them return the config?

@JaminFarr
Copy link

Just looking through the source code to see what Axios actually does. It's similar but different to what I thought.

When a request is made it creates a promise.then chain of all the request interceptors, in reverse order, ending with the dispatchRequest function.

// Starts with the config passed to axios
Promise.resolve(config)
  .then(interceptor4) // If any of the interceptors return undefined then the 
  .then(interceptor3) // next interceptor with receive undefined instead of a config
  .then(interceptor2)
  .then(interceptor1)
  .then(dispatchRequest) // Make the request

So every interceptor must return a config or the config will not get to the dispatchRequest.

I missed it out in the code above but the interceptor reject functions are also added to the chain. So interceptor1 can catch interceptor2's errors. Also the response interceptors are added, in normal order, at the end after the dispatchRequest.

I also noticed when you eject an interceptor, as I suggested, it just sets that position to null in the interceptor array. I now don't think it's a good idea to keep adding and removing interceptors because there will still be a growing internal array of nulls.

this.handlers[id] = null;

@JaminFarr
Copy link

You can add the interceptors outside of the Route function, as @crhistianramirez suggested, and set a module variable to the context values. It's a little dirty but will do the job and is similar to what you already have.

const axiosUserContext = {};

axios.interceptors.request.use(
  async config => {
    let accessToken = getAccessToken();
    let serverCallUrl = new URL(config.url);

    if (serverCallUrl.pathname.includes("/auth")) return config;

    if (accessToken && axiosUserContext.user) {
      const { exp } = jwtDecode(accessToken);

      if (Date.now() > exp * 1000) {
        if (typeof axiosUserContext.refreshAccessToken !== "function") {
          throw new Error("Access Token cannot be refreshed");
        }
        await axiosUserContext.refreshAccessToken();
        accessToken = getAccessToken();
      }

      config.headers["authorization"] = `Bearer ${accessToken}`;
      config.headers["cache-control"] = `no-cache`;
    }

    return config;
    // or throw new Error('User required')
  },
  // I don't think this function is required
  function(error) { 
    return Promise.reject(error);
  }
);

const Routes = () => {
  const { user, refreshAccessToken } = useContext(UserContext);

  axiosUserContext.user = user;
  axiosUserContext.refreshAccessToken = refreshAccessToken;

  const PrivateRoute = ({ component: Component, ...rest }) => (
    <Route
      {...rest}
      render={props =>
        user ? (
          <HomePage renderProps={props}>
            <Component />
          </HomePage>
        ) : (
          <Redirect to={"/login"} />
        )
      }
    />
  );

  return (
    <Router>
      <Switch>
        <Route exact path={"/"} render={() => <Redirect to={"/aaa"} />} />
        <Route
          path={"/login"}
          render={() => (user ? <Redirect to={"/aaa"} /> : <LogIn />)}
        />
        <PrivateRoute path="/aaa" component={Comp1} />
        <PrivateRoute path="/bbb" component={Comp2} />
        <PrivateRoute path="/ccc" component={Comp3} />
      </Switch>
    </Router>
  );
};

@ilyador
Copy link
Author

ilyador commented Aug 2, 2019

@JaminFarr Thanx. I think none of those ways are really elegant. I eventually used response interceptor which had no such problem. This just means I will need more server calls.

axios.interceptors.response.use(
  async (response) => { return response },
  async (error) => {
    const originalRequest = error.config
    const serverCallUrl = new URL(originalRequest.url)
    const status = error.response.status

    if (
      (status === 401 || status === 403) &&
      !originalRequest._retry &&
      !serverCallUrl.pathname.includes('/auth')
    ) {
      let token = await refreshAccessToken()
      setAccessToken(token)

      originalRequest._retry = true
      originalRequest.headers.authorization = `Bearer ${token}`

      return axios(originalRequest)
    }

    return Promise.reject(error)
  })

I don't close the issue though because I still hope there is a solution that can work with request interceptors inside React component.

@raDiesle
Copy link

useEffect(() => {
    const authInterceptor = axios.interceptors.request.use(function () {/*...*/});
    return () => {
        axios.interceptors.request.eject(authInterceptor);
    }
}, [user, refreshAccessToken])

Just to inform you: Registering inside of useEffect will not fire interceptor, for API calls done of child components, before first mount phase. So its very dangerous to place interceptor in this phase.

@ilyador
Copy link
Author

ilyador commented Oct 29, 2019

@reDiesle thanx, so what is the correct place to mount this interceptor?

@raDiesle
Copy link

raDiesle commented Nov 2, 2019

Out of my research I just place it in my function, so before everything is mounted.
I pass my hooks/ context / ( e.g. to show growl errors) by parameters to interceptor.

I have no issues with interceptors register again or using multiple interceptors.

function App() {
axios.interceptors.request.use(function () {/.../});
}

We have not experienced any issues with this.

@manvfx
Copy link

manvfx commented Nov 7, 2019

I Need Sample project

@subinpratap
Copy link

subinpratap commented Dec 24, 2019

Hi,

I was working on a react app using spotify's API and I had to add auth token with every call. This is how I did it.

Please refer to below project structure and code.
axios interceptor

I've declared a separate .js file where I keep all my API endpoints. There you can add specific headers required for that particular endpoint.

Let me know for any issues.

@chinesedfan
Copy link
Collaborator

Closed due to stale. And personally, I recommend @subinpratap's answer to register interceptors in a separated module (which will be only loaded once), instead of some React components.

@axios axios locked and limited conversation to collaborators May 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants