I'm trying (learning) to make an asynchronous post request using redux-saga, but i'm getting a synchronous behavior.
this is the code i'm using:
import {AUTH_REQUEST} from '../constants/authentication';
import {authSuccess, authError} from '../actions/login';
import {takeEvery, call, put, fork, all} from 'redux-saga/effects';
import axios from 'axios';
const authenticate = (username, password) => {
axios
.post('http://localhost:8000/api/auth/login/', {username, password})
.then((response) => {
console.log('RESPONSE: ', response.data);
return response.data;
})
.catch((error) => {
throw error;
});
};
function* watchAuthRequest({username, password, resolve, reject}) {
try {
const result = yield call(authenticate, username, password);
console.log('RESULT', result);
yield put(authSuccess(result));
yield call(resolve);
} catch (error) {
yield put(authError(error));
yield call(reject, {serverError: 'Something bad happend !'});
}
}
const authSaga = function* authSaga() {
yield takeEvery(AUTH_REQUEST, watchAuthRequest);
};
export default function* rootSaga() {
yield all([
fork(authSaga),
]);
};
and when i submit the form (i'm using redux-form), this is why i get in my console logs:
RESULT: undefined
RESPONSE: Object {user: Object, token: "04a06266803c826ac3af3ffb65e0762ce909b07b2373c83b5a25f24611675e00"}
and even the authSuccess action is getting dispatched with an empty payload (result)
am i doing something wrong here ?
You're missing a return:
const authenticate = (username, password) => {
return axios
.post('http://localhost:8000/api/auth/login/', {username, password})
.then((response) => {
console.log('RESPONSE: ', response.data);
return response.data;
})
.catch((error) => {
throw error;
});
};
Related
Here's what I have going:
import 'whatwg-fetch';
function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
throw(error);
})
});
};
}
function status(res) {
if (!res.ok) {
return Promise.reject()
}
return res;
}
EDIT: The promise doesn't get rejected, that's what I'm trying to figure out.
I'm using this fetch polyfill in Redux with redux-promise-middleware.
Fetch promises only reject with a TypeError when a network error occurs. Since 4xx and 5xx responses aren't network errors, there's nothing to catch. You'll need to throw an error yourself to use Promise#catch.
A fetch Response conveniently supplies an ok , which tells you whether the request succeeded. Something like this should do the trick:
fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong');
})
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
The following login with username and password example shows how to:
Check response.ok
reject if not OK, instead of throw an error
Further process any error hints from server, e.g. validation issues
login() {
const url = "https://example.com/api/users/login";
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
email: this.username,
password: this.password,
}),
})
.then((response) => {
// 1. check response.ok
if (response.ok) {
return response.json();
}
return Promise.reject(response); // 2. reject instead of throw
})
.then((json) => {
// all good, token is ready
this.store.commit("token", json.access_token);
})
.catch((response) => {
console.log(response.status, response.statusText);
// 3. get error messages, if any
response.json().then((json: any) => {
console.log(json);
})
});
},
Thanks for the help everyone, rejecting the promise in .catch() solved my issue:
export function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
return Promise.reject()
})
});
};
}
function status(res) {
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
}
For me,
fny answers really got it all. since fetch is not throwing error, we need to throw/handle the error ourselves.
Posting my solution with async/await. I think it's more strait forward and readable
Solution 1: Not throwing an error, handle the error ourselves
async _fetch(request) {
const fetchResult = await fetch(request); //Making the req
const result = await fetchResult.json(); // parsing the response
if (fetchResult.ok) {
return result; // return success object
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
const error = new Error();
error.info = responseError;
return (error);
}
Here if we getting an error, we are building an error object, plain JS object and returning it, the con is that we need to handle it outside.
How to use:
const userSaved = await apiCall(data); // calling fetch
if (userSaved instanceof Error) {
debug.log('Failed saving user', userSaved); // handle error
return;
}
debug.log('Success saving user', userSaved); // handle success
Solution 2: Throwing an error, using try/catch
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
let error = new Error();
error = { ...error, ...responseError };
throw (error);
}
Here we are throwing and error that we created, since Error ctor approve only string, Im creating the plain Error js object, and the use will be:
try {
const userSaved = await apiCall(data); // calling fetch
debug.log('Success saving user', userSaved); // handle success
} catch (e) {
debug.log('Failed saving user', userSaved); // handle error
}
Solution 3: Using customer error
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
throw new ClassError(result.message, result.data, result.code);
}
And:
class ClassError extends Error {
constructor(message = 'Something went wrong', data = '', code = '') {
super();
this.message = message;
this.data = data;
this.code = code;
}
}
Hope it helped.
2021 TypeScript Answer
What I do is write a fetch wrapper that takes a generic and if the response is ok it will auto .json() and type assert the result, otherwise the wrapper throws the response
export const fetcher = async <T>(input: RequestInfo, init?: RequestInit) => {
const response = await fetch(input, init);
if (!response.ok) {
throw response;
}
return response.json() as Promise<T>;
};
and then I'll catch errors and check if they are an instanceof Response. That way TypeScript knows that error has Response properties such as status statusText body headers etc. and I can apply a custom message for each 4xx 5xx status code.
try {
return await fetcher<LoginResponse>("http://localhost:8080/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "user#example.com", password: "passw0rd" }),
});
} catch (error) {
if (error instanceof Response) {
switch (error.status) {
case 401:
throw new Error("Invalid login credentials");
/* ... */
default:
throw new Error(`Unknown server error occured: ${error.statusText}`);
}
}
throw new Error(`Something went wrong: ${error.message || error}`);
}
and if something like a network error occurs it can be caught outside of the instanceof Response check with a more generic message i.e.
throw new Error(`Something went wrong: ${error.message || error}`);
The answer by #fny (the accepted answer) didn't work for me. The throw new Error() wasn't getting picked up by the .catch. My solution was to wrap the fetch with a function that builds a new promise:
function my_fetch(url, args) {
return new Promise((resolve, reject) => {
fetch(url, args)
.then((response) => {
response.text().then((body) => {
if (response.ok) {
resolve(body)
} else {
reject(body)
}
})
})
.catch((error) => { reject(error) })
})
}
Now every error and non-ok return will be picked up by the .catch method:
my_fetch(url, args)
.then((response) => {
// Do something with the response
})
.catch((error) => {
// Do something with the error
})
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
fetch("https://example.com/api/users")
.then(handleErrors)
.then(response => console.log("ok") )
.catch(error => console.log(error) );
I wasn't satisfied with any of the suggested solutions, so I played a bit with Fetch API to find a way to handle both success responses and error responses.
Plan was to get {status: XXX, message: 'a message'} format as a result in both cases.
Note: Success response can contain an empty body. In that case we fallback and use Response.status and Response.statusText to populate resulting response object.
fetch(url)
.then(handleResponse)
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
export const handleResponse = (res) => {
if (!res.ok) {
return res
.text()
.then(result => JSON.parse(result))
.then(result => Promise.reject({ status: result.status, message: result.message }));
}
return res
.json()
.then(result => Promise.resolve(result))
.catch(() => Promise.resolve({ status: res.status, message: res.statusText }));
};
I just checked the status of the response object:
$promise.then( function successCallback(response) {
console.log(response);
if (response.status === 200) { ... }
});
Hope this helps for me throw Error is not working
function handleErrors(response) {
if (!response.ok) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({
status: response.status,
statusText: response.statusText,
});
}, 0);
});
}
return response.json();
}
function clickHandler(event) {
const textInput = input.value;
let output;
fetch(`${URL}${encodeURI(textInput)}`)
.then(handleErrors)
.then((json) => {
output = json.contents.translated;
console.log(output);
outputDiv.innerHTML = "<p>" + output + "</p>";
})
.catch((error) => alert(error.statusText));
}
Another (shorter) version that resonates with most answers:
fetch(url)
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(json => doStuff(json)) //all good
//next line is optional
.catch(response => handleError(response)) //handle error
My question is related to customAxios.interceptors.response.use . My purpose here is; if the token expired and I got a 401 error, make a request again where I got a 401 error and write the new token to the headers. On the other hand, if I get an error except for the 401 error, show me the error.response.data . Do you think this logic is set up correctly? I tried to test but I wasn't sure especially 401 error cases
import axios from "axios";
import { LoginAPI } from "../playwright/tests/login/login.api";
import { test } from "#playwright/test"
import {configEnv} from "../config/config"
test.beforeAll(async () => {
await LoginAPI.API.Signin.run()
});
const customAxios = axios.create({
baseURL: configEnv.apiBaseURL
});
customAxios.interceptors.request.use(
async (config) => {
if (config.headers) {
config.headers['Authorization'] = `Bearer ${LoginAPI.States.token}`;
return config;
}
return config;
},
(error) => {
Promise.reject(error);
}
);
customAxios.interceptors.response.use(
function(response) {
return response;
},
async function(error) {
if (401 === error.response.status) {
await LoginAPI.API.Signin.run()
customAxios.defaults.headers.common['Authorization'] = `Bearer ${LoginAPI.States.token}`
} else {
return Promise.reject(error.response.data);
}
}
);
export default customAxios
I would recommend you to store your token in a localStorage and then replace it after refresh. This way you can set a token in your API class in one place.
import axios from "axios";
export const ApiClient = () => {
// Create a new axios instance
const api = axios.create({
baseURL: "URL",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
// Add a request interceptor to add the JWT token to the authorization header
api.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem("jwtToken");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Add a response interceptor to refresh the JWT token if it's expired
api.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
// If the error is a 401 and we have a refresh token, refresh the JWT token
if (
error.response.status === 401 &&
sessionStorage.getItem("refreshToken")
) {
const refreshToken = sessionStorage.getItem("refreshToken");
let data = JSON.stringify({
refresh_token: refreshToken,
});
post("/refreshToken", data)
.then((response) => {
sessionStorage.setItem("jwtToken", response.token);
sessionStorage.setItem("refreshToken", response.refresh_token);
// Re-run the original request that was intercepted
originalRequest.headers.Authorization = `Bearer ${response.token}`;
api(originalRequest)
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error);
});
// return api(originalRequest)
})
.catch((err) => {
// If there is an error refreshing the token, log out the user
console.log(err);
});
}
// Return the original error if we can't handle it
return Promise.reject(error);
}
);
const login = (email, password) => {
return api
.post("/authentication_token", { email, password })
.then(({ data }) => {
// Store the JWT and refresh tokens in session storage
sessionStorage.setItem("jwtToken", data.token);
sessionStorage.setItem("refreshToken", data.refresh_token);
})
.catch((err) => {
// Return the error if the request fails
return err;
});
};
const get = (path) => {
return api.get(path).then((response) => response.data);
};
const post = (path, data) => {
return api.post(path, data).then((response) => response.data);
};
const put = (path, data) => {
return api.put(path, data).then((response) => response.data);
};
const del = (path) => {
return api.delete(path).then((response) => response);
};
return {
login,
get,
post,
put,
del,
};
};
Best,
Chris
I'm attempting to get the value from a JSON response, and then use it in an axios get request. I'm receiving a garbled string, instead of the token from the userData object. How do I return the token through the function, and pass it back to the axios string? The issue seems to revolve around the return function not returning user['access-token']
componentDidMount() {
// console.log("Hello Mounted UserProfile Card!" + this.getToken())
axios
.get(`http://127.0.0.1:3000/api/v1/user_profile/${this.getToken}`)
.then(response => {
this.setState({user: response.data});
console.log("api response " + response.data)
})
.catch(function(error) {
console.log(error);
});
}
async getToken(user) {
try {
let userData = await AsyncStorage.getItem("userData");
let data = JSON.parse(userData);
let user = JSON.parse(data)
return user["access-token"].toString();
} catch(error) {
console.log("Something went wrong", error);
}
}
You are passing this.getToken to axios.get(...), where this.getToken is a method returning a Promise rather than the actual token.
You can first resolve the getToken Promise, then invoke the axios GET request.
componentDidMount() {
this.getToken()
.then(token => axios.get(`http://127.0.0.1:3000/api/v1/user_profile/${token}`))
.then(response => {
this.setState({ user: response.data });
console.log(`API response: ${response.data}`);
})
.catch(error => {
console.error(error);
});
}
async getToken() {
const userData = await AsyncStorage.getItem("userData");
const data = JSON.parse(userData);
const user = JSON.parse(data)
return user["access-token"].toString();
}
This code looks like wrong. getToken method is not calling and in this method, you are not using param user. I am not React expert but i think this can be work;
async componentDidMount() {
// console.log("Hello Mounted UserProfile Card!" + this.getToken())
try {
const token = await this.getToken();
if (token) {
const response = await axios.get(`http://127.0.0.1:3000/api/v1/user_profile/${token}`);
this.setState({user: response.data});
console.log("api response " + response.data)
} else { throw 'token not found'; }
} catch (ex) { console.log(ex); }
}
async getToken() {
try {
let userData = await AsyncStorage.getItem("userData");
let data = JSON.parse(userData);
let user = JSON.parse(data)
return user["access-token"].toString();
} catch(error) {
console.log("Something went wrong", error);
return null;
}
}
Here's what I have going:
import 'whatwg-fetch';
function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
throw(error);
})
});
};
}
function status(res) {
if (!res.ok) {
return Promise.reject()
}
return res;
}
EDIT: The promise doesn't get rejected, that's what I'm trying to figure out.
I'm using this fetch polyfill in Redux with redux-promise-middleware.
Fetch promises only reject with a TypeError when a network error occurs. Since 4xx and 5xx responses aren't network errors, there's nothing to catch. You'll need to throw an error yourself to use Promise#catch.
A fetch Response conveniently supplies an ok , which tells you whether the request succeeded. Something like this should do the trick:
fetch(url).then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong');
})
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
The following login with username and password example shows how to:
Check response.ok
reject if not OK, instead of throw an error
Further process any error hints from server, e.g. validation issues
login() {
const url = "https://example.com/api/users/login";
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify({
email: this.username,
password: this.password,
}),
})
.then((response) => {
// 1. check response.ok
if (response.ok) {
return response.json();
}
return Promise.reject(response); // 2. reject instead of throw
})
.then((json) => {
// all good, token is ready
this.store.commit("token", json.access_token);
})
.catch((response) => {
console.log(response.status, response.statusText);
// 3. get error messages, if any
response.json().then((json: any) => {
console.log(json);
})
});
},
Thanks for the help everyone, rejecting the promise in .catch() solved my issue:
export function fetchVehicle(id) {
return dispatch => {
return dispatch({
type: 'FETCH_VEHICLE',
payload: fetch(`http://swapi.co/api/vehicles/${id}/`)
.then(status)
.then(res => res.json())
.catch(error => {
return Promise.reject()
})
});
};
}
function status(res) {
if (!res.ok) {
throw new Error(res.statusText);
}
return res;
}
For me,
fny answers really got it all. since fetch is not throwing error, we need to throw/handle the error ourselves.
Posting my solution with async/await. I think it's more strait forward and readable
Solution 1: Not throwing an error, handle the error ourselves
async _fetch(request) {
const fetchResult = await fetch(request); //Making the req
const result = await fetchResult.json(); // parsing the response
if (fetchResult.ok) {
return result; // return success object
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
const error = new Error();
error.info = responseError;
return (error);
}
Here if we getting an error, we are building an error object, plain JS object and returning it, the con is that we need to handle it outside.
How to use:
const userSaved = await apiCall(data); // calling fetch
if (userSaved instanceof Error) {
debug.log('Failed saving user', userSaved); // handle error
return;
}
debug.log('Success saving user', userSaved); // handle success
Solution 2: Throwing an error, using try/catch
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
const responseError = {
type: 'Error',
message: result.message || 'Something went wrong',
data: result.data || '',
code: result.code || '',
};
let error = new Error();
error = { ...error, ...responseError };
throw (error);
}
Here we are throwing and error that we created, since Error ctor approve only string, Im creating the plain Error js object, and the use will be:
try {
const userSaved = await apiCall(data); // calling fetch
debug.log('Success saving user', userSaved); // handle success
} catch (e) {
debug.log('Failed saving user', userSaved); // handle error
}
Solution 3: Using customer error
async _fetch(request) {
const fetchResult = await fetch(request);
const result = await fetchResult.json();
if (fetchResult.ok) {
return result;
}
throw new ClassError(result.message, result.data, result.code);
}
And:
class ClassError extends Error {
constructor(message = 'Something went wrong', data = '', code = '') {
super();
this.message = message;
this.data = data;
this.code = code;
}
}
Hope it helped.
2021 TypeScript Answer
What I do is write a fetch wrapper that takes a generic and if the response is ok it will auto .json() and type assert the result, otherwise the wrapper throws the response
export const fetcher = async <T>(input: RequestInfo, init?: RequestInit) => {
const response = await fetch(input, init);
if (!response.ok) {
throw response;
}
return response.json() as Promise<T>;
};
and then I'll catch errors and check if they are an instanceof Response. That way TypeScript knows that error has Response properties such as status statusText body headers etc. and I can apply a custom message for each 4xx 5xx status code.
try {
return await fetcher<LoginResponse>("http://localhost:8080/login", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "user#example.com", password: "passw0rd" }),
});
} catch (error) {
if (error instanceof Response) {
switch (error.status) {
case 401:
throw new Error("Invalid login credentials");
/* ... */
default:
throw new Error(`Unknown server error occured: ${error.statusText}`);
}
}
throw new Error(`Something went wrong: ${error.message || error}`);
}
and if something like a network error occurs it can be caught outside of the instanceof Response check with a more generic message i.e.
throw new Error(`Something went wrong: ${error.message || error}`);
The answer by #fny (the accepted answer) didn't work for me. The throw new Error() wasn't getting picked up by the .catch. My solution was to wrap the fetch with a function that builds a new promise:
function my_fetch(url, args) {
return new Promise((resolve, reject) => {
fetch(url, args)
.then((response) => {
response.text().then((body) => {
if (response.ok) {
resolve(body)
} else {
reject(body)
}
})
})
.catch((error) => { reject(error) })
})
}
Now every error and non-ok return will be picked up by the .catch method:
my_fetch(url, args)
.then((response) => {
// Do something with the response
})
.catch((error) => {
// Do something with the error
})
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
fetch("https://example.com/api/users")
.then(handleErrors)
.then(response => console.log("ok") )
.catch(error => console.log(error) );
I wasn't satisfied with any of the suggested solutions, so I played a bit with Fetch API to find a way to handle both success responses and error responses.
Plan was to get {status: XXX, message: 'a message'} format as a result in both cases.
Note: Success response can contain an empty body. In that case we fallback and use Response.status and Response.statusText to populate resulting response object.
fetch(url)
.then(handleResponse)
.then((responseJson) => {
// Do something with the response
})
.catch((error) => {
console.log(error)
});
export const handleResponse = (res) => {
if (!res.ok) {
return res
.text()
.then(result => JSON.parse(result))
.then(result => Promise.reject({ status: result.status, message: result.message }));
}
return res
.json()
.then(result => Promise.resolve(result))
.catch(() => Promise.resolve({ status: res.status, message: res.statusText }));
};
I just checked the status of the response object:
$promise.then( function successCallback(response) {
console.log(response);
if (response.status === 200) { ... }
});
Hope this helps for me throw Error is not working
function handleErrors(response) {
if (!response.ok) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject({
status: response.status,
statusText: response.statusText,
});
}, 0);
});
}
return response.json();
}
function clickHandler(event) {
const textInput = input.value;
let output;
fetch(`${URL}${encodeURI(textInput)}`)
.then(handleErrors)
.then((json) => {
output = json.contents.translated;
console.log(output);
outputDiv.innerHTML = "<p>" + output + "</p>";
})
.catch((error) => alert(error.statusText));
}
Another (shorter) version that resonates with most answers:
fetch(url)
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(json => doStuff(json)) //all good
//next line is optional
.catch(response => handleError(response)) //handle error
Heyy so I want to get a json from a request with react-saga! I was wondering how I would get the data that the my saga yields, I have an idea to call a generator function in the componentWillMount that watches for the 'REQUEST_DONE' action with takeLatest and then rerenders.
But I think it would a bad idea to use react-saga in one of my components. Guidance please
My saga file:
export function* Saga() {
yield fetch(url, {
method: 'GET',
headers: {
'Accept': '...',
'Content-Type': 'application/json'
}
})
.then(response => {
return response.json();
})
.then(json => {
return json;
})
.catch(ex => {
console.log('parsing failed', ex)
})
}
export default function* watchAsync() {
console.log(yield Saga().next().value); // gets the value correctly
yield* takeLatest('BLAH', Saga);
}
My component
...
componentWillMount() {
const { store } = this.context;
store.dispatch({type: 'BLAH'});
// I want the request data
}
render() { ... }
EDIT webpackbin DEMO
call fetch and yield result
import { take, put, call } from 'redux-saga/effects';
function fetchData() {
return fetch(url)
.then(res => res.json() )
.then(data => ({ data }) )
.catch(ex => {
console.log('parsing failed', ex);
return ({ ex });
});
}
function* yourSaga(action) {
const { data, ex } = yield call(fetchData);
if (data)
yield put({ type: 'REQUEST_DONE', data });
else
yield put({ type: 'REQUEST_FAILED', ex });
}
export default function* watchAsync() {
yield* takeLatest('BLAH', yourSaga);
}
then connect component and slice needed data
class App extends Component {
...
componentWillMount() {
this.props.dispatch({type: 'BLAH'});
}
render(){
return (<div>Data: {this.props.data}</div>);
}
}
export default connect( state =>({
data:state.data
}))(App);