Reputation: 693
I want to make a redirection in axios interceptors when receiving a 403 error. But how can I access the history outside React components ?
In Navigating Programatically in React-Router v4, it's in the context of a React Component, but here i'm trying in axios context
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
if(error.response.status === 403) { console.log("Redirection needed !"); }
// Trow errr again (may be need for some other catch)
return Promise.reject(error);
});
Upvotes: 45
Views: 85383
Reputation: 115
Heres the modified version of the accepted answer that worked for me.
Wrap the App component in index.js using BrowserRouter otherwise the useHistory() hook wont work.
import React from 'react';
...
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter><App /></BrowserRouter>
</Provider>,
document.getElementById('root')
);
Create a seperate file instantiating the custom axios instance
import axios from 'axios';
let headers = {};
const baseURL = "http://localhost:8080"
const jwtToken = localStorage.getItem("Authorization");
if (jwtToken) {
headers.Authorization = 'Bearer ' + jwtToken;
}
const axiosInstance = axios.create({
baseURL: baseURL,
headers,
});
export default axiosInstance;
Create another file with the interceptor methods for the custom axios instance created earlier.
import axiosInstance from "./ServerAxios";
import { useHistory } from "react-router-dom";
const baseURL = "http://localhost:8080"
const SetupInterceptors = () => {
let history = useHistory();
axiosInstance.interceptors.response.use(function (response) {
return response;
}, function (error) {
var status = error.response.status;
var resBaseURL = error.response.config.baseURL;
if (resBaseURL === baseURL && status === 403) {
localStorage.removeItem("Authorization");
history.push("/login");
}
return Promise.reject(error);
});
}
export default SetupInterceptors;
Then import it and call the setup method in the App.js file
...
import { createBrowserHistory } from 'history';
import SetupInterceptors from './middleware/NetworkService';
const App = () => {
const history = createBrowserHistory();
SetupInterceptors(history);
...
Then whenever you need to use the custom axios instance, import the instantiated file and use it.
import axiosInstance from "../middleware/ServerAxios";
axiosInstance.post(......);
Upvotes: 5
Reputation: 31
UPDATE:- useHistory() is deprecated as of now. Use :-
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/login')
Upvotes: 1
Reputation: 1121
I used this approach, very simple and works like a charm:
const setupInterceptors = (navigateTo) => {
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
if(error.response.status === 401) {
navigateTo('/login');
}
return Promise.reject(error);
});
}
export default function HomePage() {
/* Use navigateTo inside your component */
const navigateTo = useNavigate();
setupInterceptors(navigateTo);
return <div></div>;
}
Upvotes: 0
Reputation: 1573
I solved this by passing useHistory()
from inside a <Router>
to axios interceptors.
App.js:
// app.js
function App() {
return (
<Router>
<InjectAxiosInterceptors />
<Route ... />
<Route ... />
</Router>
)
}
InjectAxiosInterceptors.js:
import { useEffect } from "react"
import { useHistory } from "react-router-dom"
import { setupInterceptors } from "./plugins/http"
function InjectAxiosInterceptors () {
const history = useHistory()
useEffect(() => {
console.log('this effect is called once')
setupInterceptors(history)
}, [history])
// not rendering anything
return null
}
plugins/http.js:
import axios from "axios";
const http = axios.create({
baseURL: 'https://url'
})
/**
* @param {import('history').History} history - from useHistory() hook
*/
export const setupInterceptors = history => {
http.interceptors.response.use(res => {
// success
return res
}, err => {
const { status } = err.response
if (status === 401) {
// here we have access of the useHistory() from current Router
history.push('/login')
}
return Promise.reject(err)
})
}
export default http
Upvotes: 3
Reputation: 2599
I solved this task by creating browser history from history
(https://github.com/ReactTraining/history) package and passing it into the interceptor function and then calling .push()
method from it.
The main file code (part of it):
// app.js
import { createBrowserHistory } from 'history';
import httpService from './api_client/interceptors';
...
const history = createBrowserHistory();
httpService.setupInterceptors(store, history);
Interceptor configuration:
import axios from 'axios';
export default {
setupInterceptors: (store, history) => {
axios.interceptors.response.use(response => {
return response;
}, error => {
if (error.response.status === 401) {
store.dispatch(logoutUser());
}
if (error.response.status === 404) {
history.push('/not-found');
}
return Promise.reject(error);
});
},
};
Also, you should use Router
from react-router
(https://github.com/ReactTraining/react-router) and pass the same history object as history
param.
// app.js
...
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
...
</Router>
</Provider>
, document.getElementById('#root'))
Upvotes: 27
Reputation: 67
This works perfectly.
window.location.href = `${process.env.REACT_APP_BASE_HREF}/login`;
Upvotes: 5
Reputation: 161
The accepted answer doesnt solve my problem. After spending time in axios and tickets around interceptor not triggering, i found, axios doesnt support decorating interceptor globally like it is described above. for future readers, please keep in mind that, axios has tagged this global interceptor
as feature. so maybe we will get it in the future realse. for ref: https://github.com/axios/axios/issues/993.
I do have a single axios instance for all the api call, so i solved defining interceptor in it.
Upvotes: 1
Reputation: 1333
This seems to work for me
function (error) {
var accessDenied = error.toString().indexOf("401");
if (accessDenied !== -1) {
console.log('ACCESS DENIED')
return window.location.href = '/accessdenied'
}
});
Upvotes: 5
Reputation: 71
I am using react-router-dom and it has "history" props which can be used in transition to new route
history.push('/newRoute')
Upvotes: -4
Reputation: 1249
I solved that by accessing my Redux Store from outside the Component tree and sending it my same action from the logout button, since my interceptors are created in a separated file and loaded before any Component is loaded.
So, basically, I did the following:
At index.js
file:
//....lots of imports ommited for brevity
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers';
import { UNAUTH_USER } from './actions/types'; //this is just a constants file for action types.
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);
//Here is the guy where I set up the interceptors!
NetworkService.setupInterceptors(store);
//lots of code ommited again...
//Please pay attention to the "RequireAuth" below, we'll talk about it later
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<main className="plan-container">
<Switch>
<Route exact path="/" component={Landing} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<Route exact path="/calendar" component={RequireAuth(Calendar)} />
<Route exact path="/profile" component={RequireAuth(Profile)} />
</Switch>
</main>
</div>
</BrowserRouter>
</Provider>
, document.querySelector('.main-container'));
And at the network-service.js
file:
import axios from 'axios';
import { UNAUTH_USER } from '../actions/types';
export default {
setupInterceptors: (store) => {
// Add a response interceptor
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
//catches if the session ended!
if ( error.response.data.token.KEY == 'ERR_EXPIRED_TOKEN') {
console.log("EXPIRED TOKEN!");
localStorage.clear();
store.dispatch({ type: UNAUTH_USER });
}
return Promise.reject(error);
});
}
};
Last, but not least, I have a HOC (Higher Order Component) that I wrap my protected components where I do the actual redirect when the session is out. That way, when I trigger the action type UNAUTH_USER, it sets my isLogged
property at my session
reducer to false
and therefore this component gets notified and does the redirect for me, at any time.
The file for require-auth.js
component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
export default function(ComposedComponent) {
class RequireAuth extends Component {
componentWillMount() {
if(!this.props.session.isLogged) {
this.props.history.push('/login');
}
};
componentWillUpdate(nextProps) {
if(!nextProps.session.isLogged) {
this.props.history.push('/login');
}
};
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return { session: state.session };
}
return connect(mapStateToProps)(RequireAuth);
}
Hope that helps!
Upvotes: 41
Reputation: 693
The best solution I found is to define axios.interceptors inside my main React components and use that
to handle errors :
( And with withRouter
from Router V4 )
import {withRouter} from 'react-router-dom';
class Homepage extends Component {
static propTypes = {
history: PropTypes.object.isRequired
}
constructor(props){
super(props);
let that = this;
axios.interceptors.response.use(function (response) {
// Do something with response data
return response;
}, function (error) {
// Do something with response error
if(error.response.status === 403) { that.handle403() }
// Trow errr again (may be need for some other catch)
return Promise.reject(error);
});
}
handle403(){
this.props.history.push('/login');
}
Upvotes: 1