During implementing login feature with React, Redux, isomorphic-fetch, ES6 Babel.
Questions
I do not know how to properly combine promises after the checkstatus promise in order to get parsed JSON data from my server.
what am I doing wrong here?
also, do I need to replace isomorphic-fetch package with other more convenient one?
any suggestion for other package is welcome!
loginAction.js
import * as API from '../middleware/api';
import * as ActionTypes from '../actionTypes/authActionTypes';
import 'isomorphic-fetch';
function encodeCredentials(id, pwd) {
return btoa(`${id}{GS}${pwd}`);
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
response;
} else {
const error = new Error(response.statusText);
error.response = response;
throw error;
}
}
function parseJSON(response) {
return response.json();
}
export function loginFailure(error) {
return { error, type: ActionTypes.LOGIN_FAILURE };
}
export function loginSuccess(response) {
return dispatch => {
dispatch({ response, type: ActionTypes.LOGIN_SUCCESS });
};
}
export function loginRequest(id, pwd) {
return {
type: ActionTypes.LOGIN_REQUEST,
command: 'login',
lang: 'en',
str: encodeCredentials(id, pwd),
ip: '',
device_id: '',
install_ver: '',
};
}
export function login(id, pwd) {
const credentials = loginRequest(id, pwd);
return dispatch => {
fetch(`${API.ROOT_PATH}${API.END_POINT.LOGIN}`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
})
.then(checkStatus)
.then(parseJSON)
.then(data => {
console.log(`parsed data ${data}`);
dispatch(loginSuccess(data));
})
.catch(error => {
console.log(`request failed ${error}`);
});
};
}
In my projects usually, I have a helper function fetchJSON that does all utility logic, such as JSON parsing and status check.
Here it is:
import fetch from 'isomorphic-fetch';
function checkStatus(response) {
if(response.ok) {
return response;
} else {
const error = new Error(response.statusText);
error.response = response;
throw error;
}
}
function parseJSON(response) {
return response.json();
}
export default function enhancedFetch(url, options) {
options.headers = Object.assign({
'Accept': 'application/json',
'Content-Type': 'application/json'
}, options.headers);
if(typeof options.body !== 'string') {
options.body = JSON.stringify(options.body);
}
return fetch(url, options)
.then(checkStatus)
.then(parseJSON);
}
Then you can use it in actions:
import fetchJSON from '../utils/fetchJSON'; // this is the enhanced method from utilities
export function login(id, pwd) {
const credentials = loginRequest(id, pwd);
return dispatch => {
fetchJSON(`${API.ROOT_PATH}${API.END_POINT.LOGIN}`, {
method: 'post',
body: credentials
}).then(data => {
console.log(`parsed data ${data}`);
dispatch(loginSuccess(data));
}).catch(error => {
console.log(`request failed ${error}`);
});
};
}
It helps you to keep actions code clean from some boilerplate code. In big projects with tons of similar fetch calls it is a really must-have thing.
You're doing it right, you just forgot return in checkstatus; you should return the response such that the next promise in the chain can consume it.
Also, it seems that checkstatus is synchronous operation, so it's no need to chain it by .then (although, it's OK if you like it that way), you can write:
fetch(...)
.then(response=>{
checkStatus(response)
return response.json()
})
.then(data=>{
dispatch(loginSuccess(data))
})
.catch(...)
I see no reason to get rid of isomorphic-fetch for now - it seems that it does its job.
Related
I am using Next.js. I have created an Axios interceptor where a rejected Promise will be returned. But where there is a server-specific error that I need. Next.js is showing the error in the application like this.
And there is the code of the Axios interceptor and instance.
import axios from "axios";
import store from "../redux/store";
import getConfig from 'next/config';
const { publicRuntimeConfig } = getConfig();
let token = "";
if (typeof window !== 'undefined') {
const item = localStorage.getItem('key')
token = item;
}
const axiosInstance = axios.create({
baseURL: publicRuntimeConfig.backendURL,
headers: {
Authorization: token ? `Bearer ${token}` : "",
},
});
axiosInstance.interceptors.request.use(
function (config) {
const { auth } = store.getState();
if (auth.token) {
config.headers.Authorization = `Bearer ${auth.token}`;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(res) => {
console.log(res)
return res;
},
(error) => {
console.log(error)
return Promise.reject(error);
}
);
export default axiosInstance;
Also, I am using redux and there is the action.
import axios from "../../api/axios";
import { authConstants } from "../types";
export const login = (data) => {
return async (dispatch) => {
try {
dispatch({
type: authConstants.LOGIN_REQUEST,
});
const res = axios.post("/user/login", data);
if (res.status === 200) {
dispatch({
type: authConstants.LOGIN_SUCCESS,
payload: res.data,
});
}
} catch (error) {
console.log(error, authConstants);
dispatch({
type: authConstants.LOGIN_FAILURE,
payload: { error: error.response?.data?.error },
});
}
};
};
Your problem is here...
const res = axios.post("/user/login", data);
You're missing await to wait for the response
const res = await axios.post("/user/login", data);
This fixes two things...
Your code now waits for the response and res.status on the next line will be defined
Any errors thrown by Axios (which surface as rejected promises) will trigger your catch block. Without the await this does not happen and any eventual promise failure bubbles up to the top-level Next.js error handler, resulting in the popup in your screenshot.
I have two functions that using axios post information to different APIs I created with node and express. Both of them have an interceptor as I get a response from by backend with messages, errors, and other information. Yet when I post the to the second url ("/users/login") the first interceptor still fires off (in the addUser instead of the findUser function) even though it is not in the same function. How do I fix this?
async function addUser(user) {
const config = {
headers: {
"Content-Type": "application/json",
},
};
try {
const interceptorResponse = axios.interceptors.response.use(
(response) => {
if (typeof response.data === "object") {
let success = response.data.registerSuccess;
let errors = response.data.errors;
let data = response.data.data;
let message = response.data.message;
setData(() => {
return { ...data, errors, registerSuccess: success, message };
});
}
return response;
}
);
await axios.post("/users/register", user, config);
axios.interceptors.request.eject(interceptorResponse);
} catch (err) {}
}
async function findUser(user) {
const config = {
headers: {
"Content-Type": "application/json",
},
};
try {
axios.interceptors.response.use((response) => {
console.log(response);
if (typeof response.data === "object") {
let loginSuccess = response.data.data.loginSuccess;
let message = response.data.message;
console.log(response.data);
setData(() => {
return { ...data, loginSuccess, message };
});
}
return response;
});
await axios.post("/users/login", user, config);
} catch (error) {}
}
Since I want to setup Axios interceptors with React Context, the only solution that seems viable is creating an Interceptor component in order to use the useContext hook to access Context state and dispatch.
The problem is, this creates a closure and returns old data to the interceptor when it's being called.
I am using JWT authentication using React/Node and I'm storing access tokens using Context API.
This is how my Interceptor component looks like right now:
import React, { useEffect, useContext } from 'react';
import { Context } from '../../components/Store/Store';
import { useHistory } from 'react-router-dom';
import axios from 'axios';
const ax = axios.create();
const Interceptor = ({ children }) => {
const [store, dispatch] = useContext(Context);
const history = useHistory();
const getRefreshToken = async () => {
try {
if (!store.user.token) {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
const { data } = await axios.post('/api/auth/refresh_token', {
headers: {
credentials: 'include',
},
});
if (data.user) {
dispatch({
type: 'setStore',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
authenticated: true,
token: data.accessToken,
id: data.user.id,
name: data.user.name,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: {
items: [],
count: data.user.messages,
},
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved.reduce(function (object, item) {
object[item] = true;
return object;
}, {}),
cart: {
items: data.user.cart.reduce(function (object, item) {
object[item.artwork] = true;
return object;
}, {}),
count: Object.keys(data.user.cart).length,
},
});
} else {
dispatch({
type: 'setMain',
loading: false,
error: false,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
}
} catch (err) {
dispatch({
type: 'setMain',
loading: false,
error: true,
auth: store.main.auth,
brand: store.main.brand,
theme: store.main.theme,
});
}
};
const interceptTraffic = () => {
ax.interceptors.request.use(
(request) => {
request.headers.Authorization = store.user.token
? `Bearer ${store.user.token}`
: '';
return request;
},
(error) => {
return Promise.reject(error);
}
);
ax.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
console.log(error);
if (error.response.status !== 401) {
return new Promise((resolve, reject) => {
reject(error);
});
}
if (
error.config.url === '/api/auth/refresh_token' ||
error.response.message === 'Forbidden'
) {
const { data } = await ax.post('/api/auth/logout', {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'resetUser',
});
history.push('/login');
return new Promise((resolve, reject) => {
reject(error);
});
}
const { data } = await axios.post(`/api/auth/refresh_token`, {
headers: {
credentials: 'include',
},
});
dispatch({
type: 'updateUser',
token: data.accessToken,
email: data.user.email,
photo: data.user.photo,
stripeId: data.user.stripeId,
country: data.user.country,
messages: { items: [], count: data.user.messages },
notifications:
store.user.notifications.items.length !== data.user.notifications
? {
...store.user.notifications,
items: [],
count: data.user.notifications,
hasMore: true,
cursor: 0,
ceiling: 10,
}
: {
...store.user.notifications,
count: data.user.notifications,
},
saved: data.user.saved,
cart: { items: {}, count: data.user.cart },
});
const config = error.config;
config.headers['Authorization'] = `Bearer ${data.accessToken}`;
return new Promise((resolve, reject) => {
axios
.request(config)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
);
};
useEffect(() => {
getRefreshToken();
if (!store.main.loading) interceptTraffic();
}, []);
return store.main.loading ? 'Loading...' : children;
}
export { ax };
export default Interceptor;
The getRefreshToken function is called every time a user refreshes the website to retrieve an access token if there is a refresh token in the cookie.
The interceptTraffic function is where the issue persists.
It consists of a request interceptor which appends a header with the access token to every request and a response interceptor which is used to handle access token expiration in order to fetch a new one using a refresh token.
You will notice that I am exporting ax (an instance of Axios where I added interceptors) but when it's being called outside this component, it references old store data due to closure.
This is obviously not a good solution, but that's why I need help organizing interceptors while still being able to access Context data.
Note that I created this component as a wrapper since it renders children that are provided to it, which is the main App component.
Any help is appreciated, thanks.
Common Approach (localStorage)
It is a common practice to store the JWT in the localStorage with
localStorage.setItem('token', 'your_jwt_eykdfjkdf...');
on login or page refresh, and make a module that exports an Axios instance with the token attached. We will get the token from localStorage
custom-axios.js
import axios from 'axios';
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = localStorage.getItem('token');
return config;
});
export default axiosInstance;
And then, just import the Axios instance we just created and make requests.
import axios from './custom-axios';
axios.get('/url');
axios.post('/url', { message: 'hello' });
Another approach (when you've token stored in the state)
If you have your JWT stored in the state or you can grab a fresh token from the state, make a module that exports a function that takes the token as an argument and returns an axios instance with the token attached like this:
custom-axios.js
import axios from 'axios';
const customAxios = (token) => {
// axios instance for making requests
const axiosInstance = axios.create();
// request interceptor for adding token
axiosInstance.interceptors.request.use((config) => {
// add token to request headers
config.headers['Authorization'] = token;
return config;
});
return axiosInstance;
};
export default customAxios;
And then import the function we just created, grab the token from state, and make requests:
import axios from './custom-axios';
// logic to get token from state (it may vary from your approach but the idea is same)
const token = useSelector(token => token);
axios(token).get('/url');
axios(token).post('/url', { message: 'hello' });
I have a template that works in a system with millions of access every day.
This solved my problems with refresh token and reattemp the request without crashing
First I have a "api.js" with axios, configurations, addresses, headers.
In this file there are two methods, one with auth and another without.
In this same file I configured my interceptor:
import axios from "axios";
import { ResetTokenAndReattemptRequest } from "domain/auth/AuthService";
export const api = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-Type": "application/json",
},
});
export const apiSecure = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
Authorization: "Bearer " + localStorage.getItem("Token"),
"Content-Type": "application/json",
},
export default api;
apiSecure.interceptors.response.use(
function (response) {
return response;
},
function (error) {
const access_token = localStorage.getItem("Token");
if (error.response.status === 401 && access_token) {
return ResetTokenAndReattemptRequest(error);
} else {
console.error(error);
}
return Promise.reject(error);
}
);
Then the ResetTokenAndReattemptRequest method. I placed it in another file, but you can place it wherever you want:
import api from "../api";
import axios from "axios";
let isAlreadyFetchingAccessToken = false;
let subscribers = [];
export async function ResetTokenAndReattemptRequest(error) {
try {
const { response: errorResponse } = error;
const retryOriginalRequest = new Promise((resolve) => {
addSubscriber((access_token) => {
errorResponse.config.headers.Authorization = "Bearer " + access_token;
resolve(axios(errorResponse.config));
});
});
if (!isAlreadyFetchingAccessToken) {
isAlreadyFetchingAccessToken = true;
await api
.post("/Auth/refresh", {
Token: localStorage.getItem("RefreshToken"),
LoginProvider: "Web",
})
.then(function (response) {
localStorage.setItem("Token", response.data.accessToken);
localStorage.setItem("RefreshToken", response.data.refreshToken);
localStorage.setItem("ExpiresAt", response.data.expiresAt);
})
.catch(function (error) {
return Promise.reject(error);
});
isAlreadyFetchingAccessToken = false;
onAccessTokenFetched(localStorage.getItem("Token"));
}
return retryOriginalRequest;
} catch (err) {
return Promise.reject(err);
}
}
function onAccessTokenFetched(access_token) {
subscribers.forEach((callback) => callback(access_token));
subscribers = [];
}
function addSubscriber(callback) {
subscribers.push(callback);
}
I use in my react application redux saga. There i have a login form. With redux saga i try to handle the error when user login.
Bellow is my saga:
function* postLoginUserReq(user) {
const {name} = user.values.user;
try {
const data = yield call(() => {
return fetch("url", {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
}),
}).then(data => data.json()).then(response => {
userErrorLogIn(response.error) //here i check if appears an error
})
});
} catch (error) {
console.log(error);
}
}
Bellow is action creator:
export const userErrorLogIn = (error) => {
console.log(error) //the error message appears here
return {
type: USER_ERROR_LOGIN,
payload: error
};
};
Bellow is reducer:
case USER_ERROR_LOGIN: {
console.log(action.payload) //here the error message does not appears (why?)
return {
...state,
userIsLoggedError:action.payload,
}
}
Question: What could be the issue that i don't get the error in reducer?
You can use .catch for that -
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
fetch("http://httpstat.us/500")
.then(handleErrors)
.then(response => console.log("ok") )
.catch(error => userErrorLogIn(error) );
https://www.tjvantoll.com/2015/09/13/fetch-and-errors/
This question already exists:
Vue router - beforeEach block causing 401?
Closed 2 years ago.
I have a Vue app using Vue router and I suddenly started getting a 401 on an axios.post /wp-json/jwt-auth/v1/and it only seems to be happening on OS X and iOS.
Thoughts on where to direct my debug hunt?
The error is:
{
"code": "rest_forbidden",
"message": "Sorry, you are not allowed to do that.",
"data": {
"status": 401
}
}
I post, it immediately fails with a 401, but only on Macs.
localClient config:
import axios from "axios";
import environment from "#/environments/environment";
import state from "../store";
import router from "../router";
const userData = JSON.parse(localStorage.getItem("userData"));
let instance = {};
if (userData) {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL,
headers: { Authorization: `Bearer ${userData.token}` }
});
} else {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL
});
}
instance.interceptors.request.use(
config => {
state.commit("setNetworkStatus", true);
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
response => {
state.commit("setNetworkStatus", false);
return response;
},
error => {
if ([401, 403].includes(error.response.status)) {
console.log(error);
state.commit("delUserData");
router.push("/login");
}
return Promise.reject(error);
}
);
export default {
get(path) {
return instance.get(instance.defaults.baseURL + path);
},
post(path, params) {
console.log(instance.defaults.baseURL + path, params);
return instance.post(instance.defaults.baseURL + path, params);
},
put(path, params) {
return instance.put(instance.defaults.baseURL + path, params);
},
delete(path, params) {
return instance.delete(instance.defaults.baseURL + path, params);
}
};
interceptor request success before response 401 failure:
interceptor request success=
{url: "https://panel.site.art/wp-json/jwt-auth/v1/site/transfer", method: "post", data: {…}, headers: {…}, baseURL: "https://panel.site.art/wp-json", …}
adapter: ƒ (t)
baseURL: "https://panel.site.art/wp-json"
data: "{"location_id":"rec140ttKVWJCDr8v","items":["recg1W9lQuLLRm8VS"]}"
headers:
Accept: "application/json, text/plain, */*"
Content-Type: "application/json;charset=utf-8"
__proto__: Object
maxContentLength: -1
method: "post"
timeout: 0
transformRequest: [ƒ]
transformResponse: [ƒ]
url: "https://panel.site.art/wp-json/jwt-auth/v1/site/transfer"
validateStatus: ƒ (t)
xsrfCookieName: "XSRF-TOKEN"
xsrfHeaderName: "X-XSRF-TOKEN"
__proto__: Object
A classic issue with safari & local-storage, there is a privacy config for safari which allows to disable localStorage (yeah, it is not works by the spec!)
Had the same issue in one of the companies I've worked for. Eventually, we wrote a specific flow for this case, with a product tradeoff.
It is better to save it inside a cookie in the matter explained here ReactJS - watch access token expiration
I finally figured this out by adding a billion console logs to the whole flow. What was happening is the login flow stores the user data (with token) in localStorage and Vuex store upon login. However, the way the local axios client was set up (I didn't build it) the axios instance that is used for the post was getting created WITHOUT a token, even though the token exists in state and localStorage. With a hard page refresh the axios instance is recreated with token.
So, I made the axios get, post, put, delete exports check the localStorage every time.
It's ugly as all get out, but it works. If anyone knows how to refactor this to be smaller, let me know.
import axios from "axios";
import environment from "#/environments/environment";
import state from "../store";
import router from "../router";
export default {
get(path) {
const userData = JSON.parse(localStorage.getItem("userData"));
let instance = {};
if (userData) {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL,
headers: { Authorization: `Bearer ${userData.token}` }
});
} else {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL
});
}
instance.interceptors.request.use(
config => {
state.commit("setNetworkStatus", true);
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
response => {
state.commit("setNetworkStatus", false);
return response;
},
error => {
if ([401, 403].includes(error.response.status)) {
state.commit("delUserData");
router.push("/login");
}
return Promise.reject(error);
}
);
return instance.get(instance.defaults.baseURL + path);
},
post(path, params) {
const userData = JSON.parse(localStorage.getItem("userData"));
let instance = {};
if (userData) {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL,
headers: { Authorization: `Bearer ${userData.token}` }
});
} else {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL
});
}
instance.interceptors.request.use(
config => {
state.commit("setNetworkStatus", true);
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
response => {
state.commit("setNetworkStatus", false);
return response;
},
error => {
if ([401, 403].includes(error.response.status)) {
state.commit("delUserData");
router.push("/login");
}
return Promise.reject(error);
}
);
return instance.post(instance.defaults.baseURL + path, params);
},
put(path, params) {
const userData = JSON.parse(localStorage.getItem("userData"));
let instance = {};
if (userData) {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL,
headers: { Authorization: `Bearer ${userData.token}` }
});
} else {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL
});
}
instance.interceptors.request.use(
config => {
state.commit("setNetworkStatus", true);
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
response => {
state.commit("setNetworkStatus", false);
return response;
},
error => {
if ([401, 403].includes(error.response.status)) {
state.commit("delUserData");
router.push("/login");
}
return Promise.reject(error);
}
);
return instance.put(instance.defaults.baseURL + path, params);
},
delete(path, params) {
const userData = JSON.parse(localStorage.getItem("userData"));
let instance = {};
if (userData) {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL,
headers: { Authorization: `Bearer ${userData.token}` }
});
} else {
instance = axios.create({
baseURL: environment.CUSTOM_BASE_URL
});
}
instance.interceptors.request.use(
config => {
state.commit("setNetworkStatus", true);
return config;
},
error => {
return Promise.reject(error);
}
);
instance.interceptors.response.use(
response => {
state.commit("setNetworkStatus", false);
return response;
},
error => {
if ([401, 403].includes(error.response.status)) {
state.commit("delUserData");
router.push("/login");
}
return Promise.reject(error);
}
);
return instance.delete(instance.defaults.baseURL + path, params);
}
};