In a reactive native application which is using a redux-saga architecture plus axios, I want to intercept 401 requests and dispatch an action which sends me to a login screen.
So in my axios client, I have:
axiosInstance.interceptors.response.use(
(response) => {
return response
},
(error) => {
// token expired
if (error.response.status === 401) {
console.log(`401 interceptor error: ${JSON.stringify(error)}`)
store.dispatch({type: "UNAUTHORIZED_RESPONSE_RECEIVED"})
}
return Promise.reject(error)
}
)
Now, while this works, the problem is I am having a require cycle:
Require cycle: redux/store.js -> redux/sagas.js -> redux/project/saga.js -> helpers/projectHelper.js -> helpers/client.js -> redux/store.js
This is obvious, but since to create the store I am applying the sagaMiddleware, to define it I import my sagas, in which I import the projectHelper file (which is a series of axios ajax api calls) in which I import the client which, to be able to perform the store.dispatch() needs to import the store, following the option no.1 from this series of options:
https://daveceddia.com/access-redux-store-outside-react/#option-1-export-the-store
Everything works, but this warning worries me a little bit.
Require cycles are allowed, but can result in uninitialized values. Consider refactoring to remove the need for a cycle.
My question is: how could I find other (also creative) ways to achieve what I need, which is:
intercept the 401 (not putting it into every saga action that fails)
(optional) dispatch an action which ends up ->
sending me to the "Login" Screen?
For anyone having trouble with this use case, this was the solution I adopted.
In one of my app main components (might be App.tsx), I put an Axios interceptor
componentDidMount() {
const self = this;
axios.interceptors.request.use(
function(config: AxiosRequestConfig) {
// useful to show a loader
self.props.loading(true);
return config;
},
function(error) {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
function(response) {
// loader stops
self.props.loading(false);
return response;
},
function(error) {
self.props.loading(false);
if (
typeof error.response !== "undefined" &&
error.response.status === 401
) {
console.log(`401 interceptor error: ${JSON.stringify(error)}`);
NavigationService.navigate("Login", null);
}
if (
typeof error.message !== "undefined" &&
error.message == "Network Error"
) {
console.log(`Network Error`);
}
return Promise.reject(error);
}
);
Not perfect, but I hope it could be useful for people trying to achieve this!
Related
I'm new to vue. I use interceptors for handling action responses, all easy with successful responses. But I would like to know what are the best practice to handle error responses.
I want to show a toastr with error message from response by default if there's no catch block in the action, but if there is a catch, do only catch function with no toastr shown.
Also, is it ok to handle unauthorized response making a redirect to login page directly in interceptor and what advices can be given about it?
My current interceptor looks like this:
axios.interceptors.response.use(
(response) => {
return response.data.data;
},
(error: AxiosError) => {
const data = error.response?.data;
const code = data?.code;
if (code === ErrorCodes.NEED_EMAIL_CONFIRMATION) {
router.push("email-verification").then();
} else if (code === ErrorCodes.UNAUTHORIZED) {
router.push("sign-in").then();
} else {
if (undefined !== data.error) {
toaster.error(data.error);
} else {
toaster.error(i18n.t("unexpected"));
}
}
return error;
}
);
but I don't like too many responsibilities here and I don't know how to avoid toastr show when the action has a catch function
You can control error toast notification from where you send the request, by sending an extra config.
Using axios:
axios.post('/api-name', data, {
config: {
showToast: true,
},
})
and then on axios intercept:
axios.interceptors.response.use(
response => {...},
error => {
const showTost= error.config.errorToast
if(showToast){
// show toast you can pass custom message too...<3
}
}
This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233
I'd like to intercept all api responses with the code != 200 in my main.js with the following code, dispatch an action and after that, show a toast showing the error message. I'm using vue-resource and my interceptor is the following:
Vue.http.interceptors.push(function(request, next) {
next(function(response) {
debugger;
if (response.status != 200) {
store.dispatch("errorAction", response);
}
});
});
But the code inside the callback is never reached...
And my api call is done this way. The java controller just throws an exception with the 500 error code.
Vue.http
.get(`http://${ADDRESS}/${store.state.module}/foo/exception`)
.then(() => {}, () => {});
I'm new with Promises and probably i'm messing things, but i don't want to be passing an error callback to every single promise. And what if my request is as follows:
export function getFoo(cb) {
Vue.http
.get(`http://${ADDRESS}/${store.state.module}/foo`)
.then(
response => {
return response.json();
},
() => {}
)
.then(foos => {
cb(foos);
});
}
I would like to get rid off () => {} and use the interceptor code to be run.
I think I'd had the same issue, so I tried to look at the documentation. Well, it is very simple, you just need to do something like this:
In main.js
Vue.http.interceptors.push(function(req) {
//Here you can add some headers, if needed
req.headers.set('awesomeHeader', 'owwnt')
return function(res) {
if( res.status == 200 || res.status == 201 || res.status == 202 ){ //Here you add the status codes that you'll work with, just like my example
//Sucess response
} else {
//Every time witch an request return a status differ form the list above, you can do whatever you want, for example you can redirect the page for a new one
window.location.href = `http://localhost:8080/#`
//If you want to display a notification, you need to import the component before, and then do something like it:
Notification.success({
title: 'error',
message: 'Unauthorized request!',
offset: 100
})
}
};
})
Does it answer you question? I hope it does.
I am building an SPA and I have a couple of different forms that submit data to an API. I am using axios for the ajax calls and have built a wrapper class around it for my use-case, called api. Inside that class I handle errors thrown by that instance.
The problem is I was storing an instance of the api class in each form's state. I later realized that functions shouldn't live in the state due to serialization.
The reasoning behind having the api class in the state was so that all of the children components of a form could access the errors and display their respective error along with removing the error on update.
One solution could be using an axios interceptor and commit all errors to a global errors module. But then I wouldn't know which errors belong to which form, in case two forms (or other requests) were submitted at the same time. I could of course save the errors in regard to the request URI, but then I would also have to take the request method into consideration.
reportError(state, { uri, method, errors }) {
state.errors[uri + '#' + method] = errors;
}
Then I could have a getter like:
getErrorsByRequest: state => ({ uri, method }) => {
return ...
}
But this feels unnecessarily awkward.
Since what I am trying to achieve is most likely very common, I am wondering, how do I sanely handle ajax errors reusably with Vuex?
I was checking for my old projects, and i did something similar:
This is my axios instance interceptor:
axiosInstance.interceptors.response.use(response => response, error => {
const { status } = error.response
...
...
// data invalid (Unprocessable Entity)
if (status === 422) {
// errors list from response
const dataErrors = error.response.data.errors
let objErrors = {}
// joining just the first error from array errors as value for each prop
for (let key in dataErrors) {
objErrors[key] = dataErrors[key].join()
}
// commiting to errors module
store.commit('errors/SET_ERRORS', objErrors)
}
...
...
})
Here my store module errors:
export const state = {
form_errors: {}
}
export const mutations = {
SET_ERRORS: (state, errors) => { state.form_errors = errors },
CLEAN_ERRORS: (state) => { state.form_errors = {} }
}
Using on components:
computed: {
...mapState('errors', ['form_errors'])
}
Using on form template:
<q-input
v-model="form.description"
:error-message="form_errors.description"
:error="!!form_errors.description"
/>
here is what i do, and i'am not realy sure its correct :
//store
async addUser({commit}) {
try {
const {data} = await apiService.addUser()
commit('SET_USER', data)
commit('SET_NOTIFICATION', {type:'success', message: 'user successfuly created'})
} catch (error) {
commit('SET_NOTIFICATION', {type:'error', message:error})
}
}
SET_USER(state, user) {
state.users.push(user)
}
//my component:
async addUser() {
this.isLoading = true
await this.$store.dispatch('updatePatient', this.form)
this.isLoading = false
}
is it legit ?
sometimes i think i would need more logic inside my component depending on the succes or rejected api request. Should i put all the logic in my actions ? like i do at the moment ?
Maybe should I add a status state for each actions, for example :
state {
users: []
postUserSuccess: null
postUserError: false
updateUserSuccess: null
updateUserError: false
// ...
}
and do what i want in the component with a computed property mapped to the store ?
What do you think ?
I don't know if it's a best practice but I let the components the exception handling. That method has its pros (you don't have to pollute the state with error management) and cons (you have to repeat the error management code for every action call).
All service calls will be made in actions
The state will only be set in mutations.
All service calls will return a promise with a resolve(data to load in the state) and a reject(message errors to present).
There will be an interceptor to reject the response in case there's a custom error (here you can put if the response has an error prop reject the response and send as an error the error prop, now you don't have to deconstruct the response in the action).
I'm going to give you a simplified example (I use axios, you can learn how to do it with the library that you use).
Actions in Vuex are asynchronous. So you don't need to try/catch them.
ApiService - Add User
const addUser = () => {
return new Promise((resolve, reject) => {
axios
.post(url, user)
.then(response => resolve(response.data))
.catch(error => reject(error));
});
};
store
async addUser({commit}) {
const data = await apiService.addUser();
commit('SET_USER', data);
return data;
}
if the promise in apiService.addUser is resolved the commit is going to be made if is rejected axios will return the promise and you can catch the error in the component that calls the action.
Component
async addUser() {
this.isLoading = true;
try {
await this.$store.dispatch('updatePatient', this.form);
} catch (error) {
// here goes the code to display the error or do x if there is an error,
// sometimes I store an errors array in the data of the component other times I do x logic
}
this.isLoading = false;
}
State
Your state will be cleaner now that you don't need to store those errors there.
state {
users: []
}