How can i handle axios error in redux-saga - reactjs

Is there a way to capture the error response from axios in a try/catch redux-saga?
I have the following axios logging function:
export const login = (username, password) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const body = JSON.stringify({ username, password });
return axios
.post('http://127.0.0.1:8000/accounts-api/login', body, config)
.then(response => response.data)
.catch(error => error.response.data)
};
which returns
{user: {…}, token: "b5d36ec9086f7f2403a02c2e8d70695e85a48cad2d81e39c9b31fddaa759a79e"}
when the password and username is correct or
{non_field_errors: ["Incorrect Credentials"]}
when password or username is not correct.
The login function is called in the following redux-saga generator:
export function* signIn({ payload: { username, password } }) {
try {
const response = yield login(username, password)
let { user, token } = yield response
yield console.log(user)
yield console.log(token)
} catch(e) {
yield console.log(e)
}
}
Is there a way to capture the error response from axios ( in this case {non_field_errors: ["Incorrect Credentials"]} )in redux-saga catch?
Now with incorect user credential user and token are set to undefined and function is not moving on to redux-saga catch.

Related

how to call function after async task react

i want to navigate to dashboard after login and dashboard is protected route
const handleLogin = (e) => {
e.preventDefault();
if (email || password) {
dispatch(loginUser({ email, password }));
navigate("/dashboard");
} else {
toast.error("Please Enter Email and Password");
}
};
i am using redux toolkit createAsyncThunk for api request
export const loginUser = createAsyncThunk("auth/login", async (userDetails) => {
try {
const { email, password } = userDetails;
const res = await fetch("http://localhost:5000/api/users/login", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email,
password,
}),
});
const result = await res.json();
if (result.error) {
toast.error(result.error);
} else {
toast.success("Login successfull");
localStorage.setItem("user", JSON.stringify(result));
return result;
}
} catch (error) {
console.log(error);
}
});
when i click on login it try to navigate the page before the state update what i want the navigate function wait untill the api respone recieve then navigate to dashboard
dispatch(loginUser({ email, password })); returns a promise, you can wait for the promise to resolve before doing additional work:
const handleLogin = () => {
dispatch(loginUser({ email, password })).then(() => {
navigate("/dashboard");
})
}
see Unwrapping Result Actions

Getting access token with the refresh token after expiration(JWT)

I get 401 error after a while when the page is reloaded, I figured it could be because the access token is expired. How do I set a new token with my refresh token? The below function runs every time the user visits a new page or refreshes the page. But it doesn't seem to work.
export async function currentAccount() {
if (store.get('refreshToken')) {
const query = {
grant_type: 'refresh_token',
companyId: store.get('lastCompanyId'),
refresh_token: store.get('refreshToken'),
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
})
.catch(err => {
console.log('error', err)
store.clearAll()
})
}
return false
}
Login sets the access tokens
export async function login(email, password) {
const query = {
grant_type: 'password',
username: email,
password,
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const {
data: {
access_token: accessToken,
refresh_token: refreshToken,
},
} = response
const decoded = jsonwebtoken.decode(accessToken)
response.data.authUser = decoded.authUser
const { userId, profileId, companyId } = decoded.authUser
if (accessToken) {
store.set('accessToken', accessToken)
store.set('refreshToken', refreshToken)
}
return response.data
}
return false
})
.catch(err => console.log(err))
}
saga users.js
export function* LOAD_CURRENT_ACCOUNT() {
yield put({
type: 'user/SET_STATE',
payload: {
loading: true,
},
})
const { authProvider } = yield select((state) => state.settings)
const response = yield call(mapAuthProviders[authProvider].currentAccount)
if (response) {
const decoded = jsonwebtoken.decode(response.access_token)
response.authUser = decoded.authUser
yield store.set('id', id)
try {
const user = yield call(LoadUserProfile)
if (user) {
const { company } = user
yield put({
type: 'user/SET_STATE',
payload: {
...user,
preferredDateFormat: user.preferredDateFormat || 'DD/MM/YYYY',
userId,
id,
},
})
}
} catch (error) {
}
}else{
store.set('refreshToken', response.refreshToken)
}
yield put({
type: 'user/SET_STATE',
payload: {
loading: false,
},
})
}
You can get a new access token with your refresh token using interceptors. Intercept and check for response status code 401, and get a new access token with your refresh token and add the new access token to the header.
Example:
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) { // check for the status code 401 and make call with refresh token to get new access token and set in the auth header
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
});
Simple Interceptor example,
axios.interceptors.request.use(req => {
req.headers.authorization = 'token';
return req;
});
Interceptor example for 401
axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401) {
// Fetch new access token with your refresh token
// set the auth header with the new access token fetched
}
});
There are several good posts on Interceptors usage for getting a new access token with your refresh token
https://thedutchlab.com/blog/using-axios-interceptors-for-refreshing-your-api-token
https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
Automating access token refreshing via interceptors in axios
https://stackoverflow.com/a/52737325/8370370
The above answer is good. But I found below method is better than that also using Axios Interceptors and "jwt-decode". Give it a try. (I'm using session storage for this example. You can use your own way to store the tokens securely)
Methodology
Login to get an access token and long live refresh token and then store them securely.
Create an axios instance to check the access token expiration with "jwt-decode". Then add the access token into the request if there is a valid access token, or else request a new access token using the stored refresh token and then apply the new access token into the request.
Login:
import axios from 'axios'
const handleLogin = async (login) => {
await axios
.post('/api/login', login, {
headers: {
'Content-Type': 'application/json'
}
})
.then(async response => {
sessionStorage.setItem('accessToken', response.data.accessToken)
sessionStorage.setItem('refreshToken', response.data.refreshToken)
})
.catch(err => {
if (errorCallback) errorCallback(err)
})
}
Create axios instance:
import axios from 'axios'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'
const axiosInstance = axios.create({
headers: { 'Content-Type': 'application/json' }
})
axiosInstance.interceptors.request.use(async req => {
const accessToken = sessionStorage.getItem('accessToken') ? sessionStorage.getItem('accessToken') : null
if (accessToken) {
req.headers.Authorization = `Bearer ${accessToken}`
}
const tokenData = jwt_decode(accessToken)
const isExpired = dayjs.unix(tokenData.exp).diff(dayjs()) < 1
if (!isExpired) return req
const refreshToken = sessionStorage.getItem('refreshToken')
const response = await axios.post('/api/refresh', { refreshToken }, {
headers: {
'Content-Type': 'application/json'
}
})
req.headers.Authorization = `Bearer ${response.data.accessToken}`
sessionStorage.setItem('accessToken', response.data.accessToken)
return req
})
export default axiosInstance
Use axios instance in all the requests (Redux Toolkit Example):
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
// Import axiosInstance
import axiosInstance from 'src/utils/axiosInstance'
export const getItems = createAsyncThunk(
'appItems/getItems',
async (args, { rejectedWithValue }) => {
try {
const response = await axiosInstance.get('/api/items')
return response.data
} catch ({ response }) {
return rejectedWithValue({ code: response.status, ...response.data })
}
}
)

Firebase REST API auth not working with React?

Basically a friend and I have been working on our React project and we're using Redux and Redux Thunk on the backend to handle authentication. However, we seem to have ran into an issue. Our request was working before, but now it issues a Fetch Failed Loading: POST, and doesn't continue past the initial call. However, when checking Firebase, it returns the correctly created user. I know it doesn't go past the fetch because the console.log doesn't work at all.
export const signup = (email, password) => {
return async dispatch => {
const response = await fetch('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API-KEY]',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password,
returnSecureToken: true
})
}
);
console.log(response);
If async is used,
it is recommended to use try{do await something...}catch(e){} format to write.
try{
do await something
}catch(e){
error something
}
Easy to catch errors
If async is used in the fetch. Need to do this
let response = await fetch(You_URL)
let json = await response.json()
After the response object is obtained,
it needs an await to get the body content from the response
The link below has instructions
Address: Here is the description
Since you are using fetch API, you need to do await again
export const signup = (email, password) => {
return async dispatch => {
const response = await fetch('https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API-KEY]',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
password: password,
returnSecureToken: true
})
}
);
const data = await response.json();
console.log(data );
References:
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

React Saga Generator yield call undefined object

So I am using axios to call my server and get response, and tried it with redux-saga but with no success. When I console log inside my axios call I got response, but signInUser in yield call is undefined forever. What can be wrong here?
const signInUserM = async (email, password) => {
await axios
.get("https://localhost:44320/Account/token")
.then(async function(response) {
const { data } = response;
axios.defaults.headers.common = {
Authorization: `Bearer ${data.token}`
};
await axios
.post("https://localhost:44320/Login", {
email: email,
password: password
})
.then(authUser => {
console.log(authUser); // got response
return authUser;
})
.catch(error => {
console.log(error);
return error;
});
})
.catch(error => {
console.log(error);
return error;
});
};
function* signInUserG({ payload }) {
const { email, password } = payload;
try {
const signInUser = yield call(
signInUserM,
email,
password
);
console.log(signInUser); // undefined forever
if (signInUser) {
// never gets here
yield put(userSignInSuccess(signInUser.id));
}
} catch (error) {
console.log(error);
}
}
Thanks for any help!
You forgot return in signInUserM and in front of the other await as well I think.

Using Redux-Saga how do I save my auth token?

I'm using Redux-Saga in a React Native app. When I get the authentication token back from the server, how do I save it to local storage?
I tried using
await AsyncStorage.setItem("token", token);
but React Native complained and said await was a reserved word.
Am I misunderstanding something? Is the saga code not where I should be doing this?
Here is my code
function* loginFlow(action) {
try {
let username = action.username;
let password = action.password;
const response = yield call(getUser, username, password);
let token = response.headers.get("access-token");
const result = yield response.json();
if (token) {
console.log("success: ", token);
yield put({ type: LOGIN_SUCCESS, result });
} else {
if (result.error) {
yield put({ type: LOGIN_FAILURE, error: result.error });
}
}
} catch (e) {
yield put({ type: LOGIN_FAILURE, error: e.message });
console.log("error", e);
}
}
Edit:
Here is the getUser function:
const getUser = (username, password) => {
return fetch(`${apiURL}/${apiVersion}/${apiType}/${apiEndpoint_auth}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
email: username,
password: password
})
});
};
This is how i managed to store token inside redux-saga generator.
function* loginFlow(email, password) {
try {
// get your token
const token = yield call(loginApi, email, password);
// store token to local storage
yield call(storeToken, token);
yield put({ type: LOGIN_SUCCESS });
} catch (error) {
yield put({ type: LOGIN_ERROR, error });
}
}
function loginApi(email, password) {
return fetch('https://yourApiUrl', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
.then(response => response.json())
.then(json => json)
.catch((error) => {
throw error;
});
}
async function storeToken(token) {
try {
await AsyncStorage.setItem('token', token);
} catch (error) {
console.log('AsyncStorage error during token store:', error);
}
}
Note: Store your token before you dispatch your LOGIN_SUCCESS action. So that you will get your token in React Component by re-rendering made by LOGIC_SUCCESS action.
Since each method of the AsyncStorage API returns a Promise object, you could use redux-saga call(fn, ...args) function.
From the documentation of call(fn, ...args), you could use it on a normal function that returns a Promise as a result.
Creates an Effect description that instructs the middleware to call the function fn with args as arguments.
fn: Function - A Generator function, or normal function which either returns a Promise as result, or any other value.
args: Array - An array of values to be passed as arguments to fn
In this case, we could use yield call(fn, ...args) this way:
yield call(AsyncStorage.setItem, "token", token)
This would have the same effect as await, where it would block the execution until the Promise is resolved / rejected.
Full code snippet with minor comments:
function* loginFlow(action) {
try {
let username = action.username;
let password = action.password;
const response = yield call(getUser, username, password);
let token = response.headers.get("access-token");
const result = yield response.json();
if (token) {
console.log("success: ", token);
// Wait / block until the Promise is resolved
yield call(AsyncStorage.setItem, "token", token);
// Will be only executed once the previous line have been resolved
yield put({ type: LOGIN_SUCCESS, result });
} else {
if (result.error) {
yield put({ type: LOGIN_FAILURE, error: result.error });
}
}
} catch (e) {
yield put({ type: LOGIN_FAILURE, error: e.message });
console.log("error", e);
}
}
Reference:
https://redux-saga.js.org/docs/api/#callfn-args
Remember, the await keyword is only valid inside async functions. If you use it outside of an async function's body, you will get a SyntaxError.
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
The function generator needs to have async before function.
Pass your auth token in below function.
saveToken = async (token) => {
try {
AsyncStorage.setItem("token", "" + token);
} catch (error) {
console.log('AsyncStorage error: ' + error.message);
}
}

Resources