Exception not thrown inside save saga - reactjs

I am working on an SPA with redux-saga state management. My load and save methods themselves are working, yet there is a lot of weird stuff... Below is the saga code:
export function* getEventDetails({ id }) {
const requestURL = `${url}/admin/event/${id}`
try {
const event = yield call(request, requestURL)
yield put(eventLoaded(event, id))
} catch (err) {
yield put(eventLoadingError(err))
}
}
export function* saveEventDetails({ event }) {
const id = event['id']
const requestURL = `${url}/admin/event/${
!isNaN(id) && id !== undefined && id !== null ? id : 'new'
}`
try {
const createdEvent = yield call(request, requestURL, {
method: !isNaN(id) && id !== undefined && id !== null ? 'PUT' : 'POST',
body: JSON.stringify(event)
})
yield put(eventSaved(createdEvent, createdEvent['id']))
yield put(loadEvent(createdEvent['id']))
yield put(loadPreviousEvents())
yield put(loadUpcomingEvents())
} catch (err) {
console.log('caught error inside saga')
yield put(eventSavingError(err))
}
}
export default function* eventsData() {
yield takeLatest(LOAD_EVENT, getEventDetails)
yield takeLatest(SAVE_EVENT, saveEventDetails)
}
One thing is definitely strange - if I turn off the API server then try saving, I never see caught error inside saga in the console. I am therefore unable to dispatch the eventSavingError action, etc.
Where is my error action? In the console I see:
reducer.js:48 action: {type: "project/Container/SAVE_EVENT", event: {…}}
request.js:55 PUT http://localhost:5000/event/10 net::ERR_CONNECTION_REFUSED
The request function:
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
}
const error = new Error(response.statusText)
error.response = response
throw error
}
export default function request(url, options) {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Request-Headers': 'Content-Type, Authorization'
}
const token = localStorage.getItem('token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const newOptions = {
...options,
mode: 'cors',
headers
}
return fetch(url, newOptions)
.then(checkStatus)
.then(parseJSON)
}

Using #oozywaters suggestion, I tweaked the code as:
return fetch(url, newOptions)
.then(checkStatus)
.then(parseJSON)
.catch(err => {
throw err
})
It does fix the problem with the missing exception.

Related

Request working correctly on Postman but gives a 404 error when I process it via React App

I have been trying to working with Github Gists API, specifically the "Star a Gist"functionality but I am noticing a strange behavior when I send a request via my React app.
Here's the code for the request:
const starNote = async (noteId, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
"Content-Length": "0",
},
}
try {
const response = await axios.put(`${API_URL}/${noteId}/star`, config, {
noteId: noteId,
})
console.log("request sent")
if (response.status === 204) {
console.log("working", response)
return true
}
} catch (error) {
if (error.response.status === 404) {
console.log(error)
}
}
}
And here's the code for the slice function:
export const starNote = createAsyncThunk(
"notes/starNote",
async (noteId, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.accessToken
return await notesService.starNote(noteId, token)
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString()
return thunkAPI.rejectWithValue(message)
}
}
)
The action gets triggered correctly but the request doesn't go through the:
console.log("request sent")
part and goes straight to the error. If you send a GET request, it gives a 404 error if you haven't starred a gist. But for the PUT request, why does it go straight to the error and not send the request. If i try the same with Postman it works correctly and returns a
response.status: 204
What am I doing wrong here?
Okay so what I did was insert this in the PUT request:
{ noteId: noteId }
And it worked.
Here's the complete code of the call:
const starNote = async (noteId, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
"Content-Length": "0",
},
}
try {
const response = await axios.put(
`${API_URL}/${noteId}/star`,
{ noteId: noteId },
config
)
console.log("request sent")
if (response.status === 204) {
console.log("working", response)
return true
}
} catch (error) {
if (error.response.status === 404) {
console.log(error)
}
}
}
I am still not sure why it's necessary but this is what it needed.

Axios Interceptor is not working in React JS

I am using the below code as an interceptor in my React JS app for getting token back but unfortunately, it is not working. Refresh token returns new idToken and updates local storage data correctly. The same code I'm using some other application which works fine. One main difference is that I currently use React 18 and the previous 16. I struggled to identify the problem but failed. Your help will be appreciable.
axios.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response.status === 401) {
// console.log(error.response.data.code)
let usersData = JSON.parse(localStorage.getItem("userData"));
const refreshToken = usersData.refreshToken;
return axios
.post(
`${api_base_url}/auth/authentication/refresh_token`,
JSON.stringify({
refresh_token: refreshToken,
})
)
.then((response) => {
usersData["accessToken"] = response.data.data.accessToken;
usersData["idToken"] = response.data.data.idToken;
setSessionStorage("userData", usersData);
error.response.config.headers[
"Authorization"
] = `Bearer ${response.data.data.idToken}`;
return axios(error.response.config);
})
.catch((error) => {
if (error.response.data.code !== "TOKEN_EXPIRED") {
return;
}
localStorage.clear();
window.location = "/login";
});
}
return Promise.reject(error);
}
);
function getIRequestProp(severType, isMultipart, isSocial) {
const serverUrl = severType ? social_api_base_url : api_base_url;
let userData = JSON.parse(localStorage.getItem('userData'));
let idToken;
idToken = userData !== null ? userData['idToken'] : '';
let content_type;
if (isSocial) {
content_type = 'application/x-www-form-urlencoded'
} else {
content_type = isMultipart ? 'multipart/form-data' : 'application/json'
}
return {
serverUrl: serverUrl,
requestHeader: {
'Content-Type': content_type,
'Accept-Language': DEFAULT_LANGUAGE,
Authorization: `Bearer ${idToken}`
}
};
}
async function post(url, body, isSocialServer, isMultipart) {
const {serverUrl, requestHeader} = getIRequestProp(isSocialServer, isMultipart);
return axios.post(serverUrl + url, body, {
headers: requestHeader
});
}
So, I call API like this:
AxiosServices.post(ApiUrlServices.SOCIALS_UPDATE_LINKS(UserInfo.userId), payload, false)
.then(response => {})
What i figured out that return axios(error.response.config); is not sending authorization token in API request headers and trying request infinitely. But consoling error.response.config shows token sets in the config correctly.
Adding an additional modification of axios request, I solved my problem.
axios.interceptors.request.use(request => {
// Edit request config
let usersData = JSON.parse(localStorage.getItem('userData'));
request.headers['Authorization'] = `${usersData.idToken}`;
return request;
}, error => {
return Promise.reject(error);
});

Generic function to request api with Axios

I am trying to build a generic function for my endpoints, using Axios and React. Generic because I have always the same header and I do not want to repeat a lot of code for each of my components.
To do that, I built this function (sorry, a lot of comments that I will remove after of course) :
export const getRequest = ( endpoint ) => axios
.get( env._URL_SERVER_ + endpoint, { headers: getHeaders() } )
.then((res) => {
// Success
console.log(res);
return {error: false, response: res.data};
})
.catch((error) => {
// Error
if (error.response) {
/*
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
console.log(error.response.data);
console.log(error.response.status);
return {error: true, status: error.response.status, data: error.response.data};
} else if (error.request) {
/*
* The request was made but no response was received, `error.request`
* is an instance of XMLHttpRequest in the browser and an instance
* of http.ClientRequest in Node.js
*/
console.log(error.request);
return {error: true, data: error.request };
} else {
// Something happened in setting up the request and triggered an Error
console.log('Error', error.message);
return {error: true, data: error.message}
}
});
Ant then in my components I do that :
getSchools = () => {
this.setState({
loadingSchools: true
}, () => {
getRequest(`/schools?name=${this.state.filterByName}&city=${this.state.filterByCity}&school_type_id=${this.state.filterBySchoolTypeId}&page=${this.state.selectedPage}`)
.then((response) => {
// there is an error
if (!response.error) {
this.setState({
schools: response.response.data,
meta: response.response.meta,
links: response.response.links
})
} else {
this.setState({
error: true,
errorMessage: response.data,
})
}
})
.then(() => {
this.setState({loadingSchools : false});
})
})
}
It works fine. I tested it in several situation (all is OK - 200, not found - 404, no response). But is it a good practice ? I feel that there is a lot of codes in the parent component. Maybe I complicate my life?
Here is how I've done it:
var URL_BACKEND = "http://localhost:5000/";
// Create Function to handle requests from the backend
callToBackend = async (ENDPOINT, METHOD) => {
const options = {
url: `${URL_BACKEND}${ENDPOINT}`,
method: METHOD,
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
},
};
const response = await axios(options);
return response.data;
}
// Then you make a call with the exact endpoint and method:
const response = await this.callToBackend('createSetupIntent', 'POST');
console.log(JSON.stringify(response));
create one common file for base URL let's say api.js
// api.js file code
export const apiUrl = axios.create({
baseURL: 'http://localhost:5000',
});
Register file
// register.js file code
import { apiUrl } from './api';
try {
const resp = await apiUrl.post('/api/register', {
username,
email,
password,
});
const { data, status } = resp;
if (Object.keys(data).length && status === 200) {
// received api data successfully
console.log('API response', data);
}
} catch (err) {
console.log(err);
}
// For auth request
try {
const token = localstorage.getItem('token');
const res = await apiUrl.post(
'/authroute',
{
name: fullName,
originCountry: country,
career: careerStatus,
},
{
headers: { Authorization: `Bearer ${token}` },
}
);
const { data, status } = strapiRes;
if (Object.keys(data).length && status === 200) {
return res.status(status).json(data);
}
} catch (error) {
throw new Error(error);
}
// same for all request
apiUrl.get(endpoint);
apiUrl.post(endpoint, body);
apiUrl.put(endpoint, body);
apiUrl.delete(endpoint, body);

Fulfilled promise stored rather than the request body

The following code sends a post request to an API and then stores the API's response. The object stored is a fulfilled promise, rather than the body. I though I was using the .then properly, as something similar works for a get request.
The following saga is called every time a post request is made. It contains everything from the post to the call to the call to the reducer.
function* setData(action) {
const url = action.payload.url;
const data_obj = action.payload.data;
console.log(action);
try {
const json = fetch(url, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data_obj)
})
.then(statusHelper)
.then(response => response.json());
yield put({ type: "DATA_SENT", payload: json });
}
catch (e)
{
yield put({ type: ERROR_OCCURED, payload: { error : {message: "An Error occured sending HIT data. Please try again later"}}});
}
}
Below is the statusHelper function. It came from another SO answer (I'll edit in the link).
function statusHelper (response) {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
} else {
return Promise.reject(new Error(response.statusText))
}
}
I think your issue is here
.then(response => response.json());
response.json() also returns a promise.
You need to .then off the json() call and return the data.
I was missing a yield after the fetch. This wound up being the solution for me
function* setData(action) {
const url = action.payload.url;
const data_obj = action.payload.data;
let json;
try {
const response = yield fetch(url, {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(data_obj)
});
//const resp = statusHelper(response);
console.log('Response: ', response, response.status)
if(response.status >= 200 && response.status < 300)
{
json = yield response.json();
}
console.log("JSON: ", json);
}
catch (e)
{
yield put({ type: ERROR_OCCURED, payload: { error : {message: "An Error occured sending HIT data. Please try again later"}}});
return;
}
yield put({ type: "DATA_SENT", payload: json });
}

How to handle common fetch actions inside saga

I'm developping an API consuming web front site.
The problem
All my API saga were like this :
export function* login(action) {
const requestURL = "./api/auth/login"; // Endpoint URL
// Select the token if needed : const token = yield select(makeSelectToken());
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + btoa(JSON.stringify({ login: action.email, password: action.password })),
}
};
try {
// The request helper from react-boilerplate
const user = yield call(request, requestURL, options);
yield put(loginActions.loginSuccess(user.token);
yield put(push('/'));
} catch (err) {
yield put(loginActions.loginFailure(err.detailedMessage));
yield put(executeErrorHandler(err.code, err.detailedMessage, err.key)); // Error handling
}
}
And I had the same pattern with all my sagas :
Select the token if I need to call a private function in the start of the saga
const token = yield select(makeSelectToken());
Handle errors on the catch part
export const executeErrorHandler = (code, detailedMessage, key) => ({
type: HTTP_ERROR_HANDLER, status: code, detailedMessage, key
});
export function* errorHandler(action) {
switch (action.status) {
case 400:
yield put(addError(action.key, action.detailedMessage));
break;
case 401:
put(push('/login'));
break;
//other errors...
}
}
export default function* httpError() {
yield takeLatest(HTTP_ERROR_HANDLER, errorHandler);
}
The solution I came up with
Remove the token parts and error handling part and puth them inside the call helper :
export function* login(action) {
const url = `${apiUrl.public}/signin`;
const body = JSON.stringify({
email: action.email,
password: action.password,
});
try {
const user = yield call(postRequest, { url, body });
yield put(loginSuccess(user.token, action.email));
yield put(push('/'));
} catch (err) {
yield put(loginFailure());
}
}
// post request just call the default request with a "post" method
export function postRequest({ url, headers, body, auth = null }) {
return request(url, 'post', headers, body, auth);
}
export default function request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
return fetch(url, addHeader(options, auth)) // add header will add the token if auth == true
.then(checkStatus)
.then(parseJSON)
.catch(handleError); // the error handler
}
function handleError(error) {
if (error.code === 401) {
put(push('/login')); // <-- Here this doesn't work
}
if (error.code == 400) {
displayToast(error);
}
}
function addHeader(options = {}, auth) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (auth) {
const token = yield select(makeSelectToken()); // <-- here it doesn't work
newOptions.headers.Authorization = `Bearer ${auth}`;
}
return newOptions;
}
I know the solution is between generator functions, side effects, yield call / select but I tried so many things it didn't work. For example, if I wrap everything inside generator functions, the token load is executed after the code continues and call the API.
Your help would be appreciated.
You need to run any and all effects (e.g. yield select) from a generator function, so you'll need generators all the way down to the point in your call stack where you yield an effect. Given that I would try to push those calls as high as possible. I assume you may have getRequest, putRequest etc. in addition to postRequest so if you want to avoid duplicating the yield select you'll want to do it in request. I can't fully test your snippet but I believe this should work:
export function* postRequest({ url, headers, body, auth = null }) {
return yield call(request, url, 'post', headers, body, auth); // could yield directly but using `call` makes testing eaiser
}
export default function* request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
const token = auth ? yield select(makeSelectToken()) : null;
try {
const response = yield call(fetch, url, addHeader(options, token));
const checkedResponse = checkStatus(response);
return parseJSON(checkedResponse);
} catch (e) {
const errorEffect = getErrorEffect(e); // replaces handleError
if (errorEffect) {
yield errorEffect;
}
}
}
function addHeader(options = {}, token) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (token) {
newOptions.headers.Authorization = `Bearer ${token}`;
}
return newOptions;
}
function getErrorEffect(error) {
if (error.code === 401) {
return put(push('/login')); // returns the effect for the `request` generator to yeild
}
if (error.code == 400) {
return displayToast(error); // assuming `displayToast` is an effect that can be yielded directly
}
}

Resources