What I want is to make GET http request in my provider, wrap http call in Observable.create and return Observable object, then in my component, resolve that Observable. I tried something, can someone tell me if it is ok (I can't test at the moment so don't know if it works)? If it's not ok, can someone guide me what is the best practice in Ionic 3, to do http call and handle response (success, failure)?
In provider:
public getUsers(credentials):Observable<any> {
if("some custom condition not satisfied")
return Observable.throw("ERROR);
return Observable.create(observer => {
this.http.get(API_URL + '/users').subscribe(
(response) => {
observer.next(response);
observer.complete();
},
(error)=> {
return Observable.throw(error);
});
});
}
then in component:
this.myProvider.login().subscribe(response => {
if (response.status == 403) {
this.loading.dismiss();
console.log("Access denied");
}
this.loading.dismiss();
this.nav.setRoot('MainPage');
},
error => {
this.loading.dismiss();
console.log(error);
});
Related
I am able to add an interceptor for the Axios pipeline. Also, I need the loader to be conditional based. The situation is some requests can run in the background and don't need a loader to be blocking the UI. In such cases, I will be able to let the Axios know by sending an extra parameter saying isBackground call. How can I achieve this?
axios.interceptors.request.use((config) => {
this.isLoading = true; // Or trigger start loader
return config
}, (error) => {
this.isLoading = false // Or trigger stoploader
return Promise.reject(error)
})
axios.interceptors.response.use((response) => {
this.isLoading = false // Or trigger stoploader
return response
}, function(error) {
this.isLoading = false // Or trigger stoploader
return Promise.reject(error)
})
Just use your own custom property isBackground on the config like this:
axios.interceptors.request.use((config) => {
console.log(config.isBackground)
return config
}, (error) => {
console.log(error.config.isBackground)
return Promise.reject(error)
})
axios.interceptors.response.use((response) => {
console.log(response.config.isBackground)
return response
}, function(error) {
console.log(error.config.isBackground)
return Promise.reject(error)
})
const config = {
isBackground: true
}
axios.get('https://httpbin.org/get', config)
.then(response => {
console.log(response)
})
.catch(error => {
console.log(error)
})
Note that there is a bug in current release 0.19.0 waiting to be fixed, which breaks this functionality. Works ok in version 0.18...
Fiddle
I'm receiving data from api using axios, and i want to add error handling to it, i have different messages on different error codes received(401,400,503 etc), but i also want to set timeout of 1 min on 401 errors 2min on error code 500 etc.is that possible using axios without importing axios retry ? I tried coming up with a solution by using interceptors but then spread.js which is imported with axios spams the error in console log and causes browser freeze.I used the following code for timeout in interceptors
axios.interceptors.response.use(function (response) {
// Do something with response data
console.log(response);
return response;
}, function (error) {
// Do something with response error
if(error.response.status===401){
setTimeout(function (){
console.log(error.response.status===401);
return Promise.reject(error);
},10000);
}
if(error.response.status===503){
setTimeout(function (){
console.log(error.response.status===503);
return Promise.reject(error);
},30000);
}
if(error.response.status===500){
setTimeout(function (){
console.log(error.response.status===401);
return Promise.reject(error);
},50000);
}
});
You need to return your setTimeout() in order for it to work. You could use else-ifs in your code to make it more efficient
axios.interceptors.response.use(function(response) {
console.log(response);
return response;
}, function(error) {
if (error.response.status === 401) {
return setTimeout(function() {
console.log(error.response.status === 401);
return Promise.reject(error);
}, 10000);
}
else if (error.response.status === 503) {
return setTimeout(function() {
console.log(error.response.status === 503);
return Promise.reject(error);
}, 30000);
}
else if (error.response.status === 500) {
return setTimeout(function() {
console.log(error.response.status === 401);
return Promise.reject(error);
}, 50000);
}
else {
return Promise.reject(error)
}
});
With axios the code is:
export const createBlaBla = (payload) => {
return axios.post('/some-url', payload)
.then(response => response)
.catch(err => err);
}
And then I'm using this with redux-saga like this:
function* createBlaBlaFlow(action) {
try {
const response = yield call(createBlaBla, action.payload);
if (response) {
yield put({
type: CREATE_BLA_BLA_SUCCESS
});
}
} catch (err) {
// I need the error data here ..
yield put({
type: CREATE_BLA_BLA_FAILURE,
payload: 'failed to create bla-bla'
});
}
}
In case of some error on the backend - like invalid data send to the backend - it returns a 400 response with some data:
{
"code":"ERR-1000",
"message":"Validation failed because ..."
"method":"POST",
"errorDetails":"..."
}
But I don't receive this useful data in the catch statement inside the saga. I can console.log() the data in the axios catch statement, also I can get it inside the try statement in the saga, but it never arrives in the catch.
Probably I need to do something else? ... Or the server shouldn't return 400 response in this case?
So, I came up with two solutions of this problem.
===
First one - very dump workaround, but actually it can be handy in some specific cases.
In the saga, right before we call the function with the axios call inside, we have a variable for the errors and a callback that sets that variable:
let errorResponseData = {};
const errorCallback = (usefulErrorData) => {
errorResponseData = usefulErrorData;
};
Then - in the axios method we have this:
export const createBlaBla = (payload, errCallback) => {
return axios.post('/some-url', payload)
.then(response => response)
.catch(err => {
if (err && err.response.data && typeof errCallback === 'function') {
errCallback(err.response.data);
}
return err;
});
}
This way, when we make request and the backend returns errors - we'll call the callback and will provide the errors from the backend there. This way - in the saga - we have the errors in a variable and can use it as we want.
===
However, another solution came to me from another forum.
The problem I have is because in the method with the axios call I have catch, which means that the errors won't bubble in the generator. So - if we modify the method with the axios call like this:
export const createBlaBla = (payload) => {
return axios.post('/some-url', payload)
}
Then in the catch statement in the saga we'll have the actual backend error.
Hope this helps someone else :)
In your API call you can do the following:
const someAPICall = (action) => {
return axios.put(`some/path/to/api`, data, {
withCredentials: true,
validateStatus: (status) => {
return (status == 200 || status === 403);
}
});
};
Please note the validateStatus() part - this way when axios will encounter 200 or 403 response, it will not throw Error and you will be able to process the response after
const response = yield call(someAPICall, action);
if (response.status === 200) {
// Proceed further
} else if (response.status === 403) {
// Inform user about error
} else {
...
}
I have the following code in my service:
public loginWithFacebook(): Observable<any> {
console.log('Login');
return Observable.fromPromise(this.fb.login()).flatMap((userData) => {
return this.http.post(authFacebook, {access_token: userData.authResponse.accessToken}, { observe: 'response' });
}).do( (response: HttpResponse<any>) => {
const token = response.headers.get('x-auth-token');
if (token) {
localStorage.setItem('id_token', token);
}
});
}
getFacebookProfile():Observable<any> {
console.log("Get Profile");
return Observable.fromPromise(this.fb.getLoginStatus())
.filter((state) => state.status === 'connected')
.switchMapTo(Observable.fromPromise(this.fb.api('/me')));
}
And later I use it in my component to get the profile info once login is successful.
this.profileData = this.usersService.loginWithFacebook()
.flatMapTo(this.usersService.getFacebookProfile());
However, for some reason getFacebookProfile() fires instantly even before the login procedure is complete. And I get an authentication error. Also, I have to login twice to get profile info displayed.
I've been always thinking that switchMap and flatMap only switch to the next observable once the previous one emits a value.
What am I doing wrong here?
--EDIT--
If I subscribe to the first Observable and call getFacebookProfile() in the subscription, everything works normally. But it's not very elegant solution I feel.
The problem is that promises are eager. You are calling this.fb.login() when you compose your observable and you are passing the returned promise into fromPromise.
That means that the login is initiated when loginWithFacebook is called and not when subscribe is called on the observable it returns.
If you want the login to be deferred until subscribe is called, you can use defer:
public loginWithFacebook(): Observable<any> {
console.log('Login');
return Observable.defer(() => Observable.fromPromise(this.fb.login()))
.flatMap((userData) => {
return this.http.post(authFacebook, {
access_token: userData.authResponse.accessToken
}, { observe: 'response' });
})
.do( (response: HttpResponse<any>) => {
const token = response.headers.get('x-auth-token');
if (token) {
localStorage.setItem('id_token', token);
}
});
}
For more information on using observables and promises, see Ben Lesh's article: RxJS Observable interop with Promises and Async-Await
It worked at last thanks to #cartant's answer. However, for some reason, I had to wrap it with defer operator twice. I would be thankful if someone could explain why it was necessary to do it. It's a bit weird.
public loginWithFacebook(): Observable<any> {
return Observable.defer(() =>
Observable.defer(() => this.fb.login()).flatMap((userData) =>
{
return this.http.post(authFacebook, {access_token: userData.authResponse.accessToken}, { observe: 'response' });
}).do( (response: HttpResponse<any>) => {
const token = response.headers.get('x-auth-token');
if (token) {
localStorage.setItem('id_token', token);
}
})
);
}
getFacebookProfile():Observable<any> {
return Observable.defer(() =>
Observable.defer(() => this.fb.getLoginStatus())
.filter((state) => state.status === 'connected')
.switchMapTo(Observable.fromPromise(this.fb.api('/me')))
);
}
Using $http I can catch errors like 401 easily:
$http({method: 'GET', url: 'http://localhost/Blog/posts/index.json'}).
success(function(data, status, headers, config) {
$scope.posts = data;
}).
error(function(data, status, headers, config) {
if(status == 401)
{
alert('not auth.');
}
$scope.posts = {};
});
But how can I do something similar when using services instead. This is how my current service looks:
myModule.factory('Post', function($resource){
return $resource('http://localhost/Blog/posts/index.json', {}, {
index: {method:'GET', params:{}, isArray:true}
});
});
(Yes, I'm just learning angular).
SOLUTION (thanks to Nitish Kumar and all the contributors)
In the Post controller I was calling the service like this:
function PhoneListCtrl($scope, Post) {
$scope.posts = Post.query();
}
//PhoneListCtrl.$inject = ['$scope', 'Post'];
As suggested by the selected answer, now I'm calling it like this and it works:
function PhoneListCtrl($scope, Post) {
Post.query({},
//When it works
function(data){
$scope.posts = data;
},
//When it fails
function(error){
alert(error.status);
});
}
//PhoneListCtrl.$inject = ['$scope', 'Post'];
in controller call Post like .
Post.index({},
function success(data) {
$scope.posts = data;
},
function err(error) {
if(error.status == 401)
{
alert('not auth.');
}
$scope.posts = {};
}
);
Resources return promises just like http. Simply hook into the error resolution:
Post.get(...).then(function(){
//successful things happen here
}, function(){
//errorful things happen here
});
AngularJS Failed Resource GET
$http is a service just like $resource is a service.
myModule.factory('Post', function($resource){
return $http({method: 'GET', url: 'http://localhost/Blog/posts/index.json'});
});
This will return the promise. You can also use a promise inside your factory and chain that so your factory (service) does all of the error handling for you.