I needed to catch a possible login page in all responses from the server, so I have overridden Backbone.sync globally so I can check all errors before passing them on.
Backbone.originalSync = Backbone.sync;
Backbone.sync = function (method, model, options) {
var originalSuccess, originalError;
console.log("sync override...");
// remember original success so we can call it if user logs in successfully
originalSuccess = options.success;
// proxy the error callback to first check if we get a login page back
originalError = options.error;
options.error = function (model, xhr, options) {
if (xhr.status === 200 && xhr.responseText === "") {
// parse error from empty response (jq1.9 invalid json, ok)
originalSuccess(model, xhr, options);
} else {
console.log("Sync error " + statusTxt + ", " + thrown.message);
if (xhr.status === 200 || xhr.status === 302 || xhr.status === 0) {
// login page returned instead of json...
// open a new window with relogon.html to trigger a new login
window.showModalDialog("../relogon.html");
} else {
// normal error, pass along
if (originalError) {
originalError(model, xhr, options);
}
}
}
};
// call the original sync
Backbone.originalSync(method, model, options);
};
This broke miserably when going from 0.9.9 to 1.0. Looks like the original Backbone.sync wraps its error handlers differently, causing my error handler to be called first, with a jquery xhr signature.
I had to change the signature of the error handler to this:
options.error = function (xhr, statusTxt, thrown) {
Ok so now it works, but I get the feeling that I am doing something wrong.
Is there a better way to do this?
I tried with jquery promises but I need to be able to switch from error state to success (when calling originalSuccess), which did not seem to work with promises.
All sync errors are passed to model's error event, so you may to listen to this event.
From http://backbonejs.org/#Events-catalog:
"error" (model, xhr, options) — when a model's save call fails on the server.
To capture error globally you may use http://api.jquery.com/ajaxError/
You can build your own jQuery Deferred object to alter the default Backbone.sync behavior
Backbone.sync = function (method, model, opts) {
var xhr, dfd;
dfd = $.Deferred();
// opts.success and opts.error are resolved against the deferred object
// instead of the jqXHR object
if (opts)
dfd.then(opts.success, opts.error);
xhr = Backbone.originalSync(method, model, _.omit(opts, 'success', 'error'));
// success : forward to the deferred
xhr.done(dfd.resolve);
// failure : resolve or reject the deferred according to your cases
xhr.fail(function() {
if (xhr.status === 200 && xhr.responseText === "") {
dfd.resolve.apply(xhr, arguments);
} else {
if (xhr.status === 200 || xhr.status === 302 || xhr.status === 0) {
console.log('login');
}
dfd.reject.apply(xhr, arguments);
}
});
// return the promise to add callbacks if necessary
return dfd.promise();
};
The promise reflects the end state you choose.
http://jsfiddle.net/AsMYQ/4/ for a fail demo, http://jsfiddle.net/AsMYQ/5/ for a success.
And if I may
you probably should not tie so tightly Backbone.sync with your login handling. Use events, from Backbone or jQuery.ajaxError as #Andrey suggested
your server response should indicate an authorization failure, probably a 401 status
don't forget to return your deferred promise/jqXHR object when you override sync, that might come in handy down the line
Related
In my website, I have API's for twitter and facebook which enables the "mentions" feature (the one that pops up whenever we use the # symbol)
However, it is often that the access token for some feature is often expires resulting in the API not working. I store all my API's in an array and then I need to check if the token has failed or not resulting in resolved or rejected API promise.
This is an older code that needs changing due $q.all. As $q.all works whenever all the promises have been resolved, thus triggering the .then() call, this results in the .then() function NEVER working in my case (as the Facebook API is never working)
I need to find a condition where each API is checked and the .then() runs for only that API that is resolved (twitter in this case) and ignores the failed API (Facebook in this case)
if (selectedIds.allowed.TW) {
usersApi.push(TS.loginResource.getTwitterProfiles({
subUserId: selectedIds.allowed.TW,
name: searchTerm
}).$promise);
}
if (selectedIds.allowed.FB || selectedIds.allowed.FB_PAGE ||
selectedIds.allowed.FB_GROUP) {
$scope.post.showTags = true;
usersApi.push(TS.loginResource.getFbPages({
subUserId: selectedIds.allowed.FB_PAGE || selectedIds.allowed.FB
|| selectedIds.allowed.FB_GROUP,
name: searchTerm
}).$promise);
}
if (usersApi.length) {
$q.all(usersApi).then(function (responses) {
var tags1 = responses[0];
tags1.forEach(function (tag, i) {
tags1[i].name = tag.name.replace(/\"/g, "");
});
$scope.post.tags = tags1;
if (usersApi.length > 1) {
var tags2 = responses[1]
tags2.forEach(function (tag, i) {
tags2[i].name = tag.name.replace(/\"/g, "");
});
$scope.post.tags = $scope.post.tags.concat(tags2);
}
})
}
}, 500);
} else {
$scope.post.tags = [];
$scope.post.showTags = false;
}
I think you are looking to chain backup response that catch the api error and return a new success resolved promise on each specific api call before you wait on "all" of them.
apiCalls.push(doTwiterStuff().then(handleTwiterSuccess, handleApiFailure);
apiClass.push(doFBStuff().then(handleFbSuccess, handleApiFailure);
Promise.all(apiCalls).then(arr => {
arr.filter(x => !isNil(x)).forEach(x => doSomethingWithApiResult(x));
});
function handleApiFailure(x) {
...
return Promise.resolve(null);
}
Hopes this helps.
$q.all is not resilient1
If one of the promises is rejected, the $q.all is rejected with the first error.
To create a resilient composite promise, that is a promise that waits for all the promises to complete pass or fail, use .catch on each individual promise to convert the rejected promise to a successful promise.
var resilientPromises = [];
angular.forEach(promises, function(p) {
var resilientP = p.catch( function(result) {
//return to convert rejection to success
return result;
});
resilientPromises.push(resilientP);
});
$q.all(resilientPromises).then( function (results) {
//process results
});
The two things to take away from this answer:
A $q.all promise is not resilient. It is rejected with the first rejected promise.
A fulfilled promise can be created from a rejected promise by returning a value to the onRejected function of either the .then method or the .catch method.
I thought that I had this all figured out on previous projects through the years.. Apparently not.
Goal : Take Service that calls other Services and if there is any type of error being returned ( not a status of 200 ) then I need the async thing to be waiting and not proceeding.
Seems to me like I don't ever see really that great of examples as it is all very simplistic.
I read various articles about what Angular (1) is doing under the hood , and i see that there are $q, .then, .success etc..
Seems that I am having issues with return and with other nested and bundled service calls being made without any checking of a problem.
Essentially this image shows what is coming back
data : null ( that is bad)
errorList Array1 0 "This order cannot be submitted... " ( bad too)
hasErrors : true ( bad as well)
So that data is important to me to capture and display to user and then NOT move onto more processing
This is my order of operations
this.submitEnrollment = function (enrollment) {
return getSubmit(requestData);
}
// Which CALLS below
var getSubmit = function (request) {
return SparkRequestService
.submitRequest(request)
.then(
function (resData) {
console.log("resData", resData);
enrollmentService.resetEnrollment();
return resData;
},
function (resData) {
console.log('error');
}
);
}
Then I'm certainly calling SparkRequestService.submitRequest(request)
but based on the image attached, I am getting the error in the resData
So, it seems that I need to interrogate the resData right? So then I really should NOT ALLOW this other service to be called enrollmentService.resetEnrollment();
How can i refactor to stop from that getting processed? if statement in the .then ?
To prevent a rejection handler from converting a rejected promise to a fulfilled promise it is important use a throw statement in the rejection handler:
var getSubmit = function (request) {
return SparkRequestService
.submitRequest(request)
.then(
function (resData) {
console.log("resData", resData);
enrollmentService.resetEnrollment();
return resData;
},
function (errorResponse) {
console.log('error');
//IMPORTANT
//throw to chain rejection
throw errorResponse;
}
);
}
When a function omits a return or throw statement, the function returns a value of undefined. This will convert a rejected promise to a fulfilled promise that resolves with a value of undefined.
Problem is that ... it is a business error wrapped up in a return object
To convert a fulfilled promise to a rejected promise, use a throw statement.
this.submitEnrollment = function (enrollment) {
var promise = getSubmit(requestData);
var newPromise = promise.then(function(response) {
if (response.data.hasErrors) {
console.log(response.data.errorList);
response.data.errorList.push("submitEnrollent: Rejected");
//THROW to create rejection
throw response;
} else {
//RETURN response to chain success
return response;
}
});
return newPromise;
}
When a promise is converted to a rejection, all subsequent success handlers in the chain will be skipped. The chain will be followed until a rejection handler is found.
it seems that the promise returned by SparkRequestService.submitRequest(request) is not rejected when you get the error inside resData. For this reason the successCallback of then is invoked and not the second one, the errorCallback.
For this reason, inside the successCallback you need to inspect the data of resData to check errors and behave accordingly, for example:
var getSubmit = function (request) {
return SparkRequestService
.submitRequest(request)
.then(function (resData) {
console.log("resData", resData);
if(resData === null) { // Check for errors
// There is an error, manage it inside this block of code
// ...
// You can also create a rejected promise with $q.reject() and passing resData containing the errors
return $q.reject(resData);
} else {
// Call resetEnrollment() in the ELSE branch so, it is executed only if resData does not contain errors
enrollmentService.resetEnrollment();
return resData;
}
},
function (resData) {
console.log('error');
}
);
};
I am in process of creating Offline mode in an app. Whenever a http request is send, I would want an interceptor to detect the network state. If the state is no connectivity, I would want to create a mock response and make it feel like as if the response is coming from a server.
You could check if you are online or not by reading the status of the response in your interceptor, if its a 401 / 501/ etc:
var interceptor = ['$rootScope', '$q', function ($rootScope, $q) {
function success(response) {
return response;
}
function error(response) {
var status = response.status; // error code
if ((status >= 400) && (status < 500)) {
$rootScope.broadcast("AuthError", status);
return;
}
if ((status >= 500) && (status < 600)) {
$rootScope.broadcast("ServerError", status);
return;
}
// otherwise
return $q.reject(response);
}
return function (promise) {
return promise.then(success, error);
}
}]
There's another way, using html5, but I guess will not work on some browsers. This is done using navigator.onLine property, like:
if (navigator.onLine) {
//I'm online
} else {
I'm not online
}
(https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine)
If you throw an object inside your request interceptor this will trigger a call to the responseError interceptor, passing the throwed object as its argument.
You have to find a way notify responseError interceptor that this is not a real error and that it should return a custom response.
The responseError interceptor can choose to recover error returning a response object o fail returning a rejected promise (check angular's $http interceptor docs)
EDIT: If you throw an object from request interceptor you can't avoid an error message in console. It's better to return a rejected promise passing config object as value and put extra information in it for responseError detect the special situation.
I have a couple chained $http combined with a single $http using $q.all([prmiseA, promiseB]). Everything is working fine, I get the data back and errors are handled no problem.
Except that on occasion data won't be found on a particular http call and it is not an error.
I am using a service to separate the logic from the UI. And my call looks like this
$scope.Loading = true;
var p = Service.Call(Param1, Param2);
p.then(function () {
$scope.Loading = false;
}, function (reason) {
$scope.Loading = false;
$scope.alerts.push({ msg: "Error loading information " + Param1, type: "danger" });
})
What I would like to be able to do is handling the 404 on that one URL inside the 'Service.Call' function. So that the UI code above remains untouched.
My problem is that if I add an error handler to the specific call that may return a 404. Then all errors are "handled" and so I loose errors for that one call.
Is there a way to "reraise" in $q?
Is there a way to "reraise" in $q?
Yes, you can rethrow by returning a rejected promise from the handler:
return $q.reject(new Error("Re Thrown")); // this is an actual `throw` in most
// promise implemenentations
In case an $http call 404 is not an error, you can recover from it. One of the cool features of promises is that we get to recover from errors:
var makeCallAndRecover(url){
return $http.get(...).catch(function(err){
// recover here if err is 404
if(err.status === 404) return null; //returning recovery
// otherwise return a $q.reject
return $q.reject(err);
});
}
I implemented Primus (Sockets) on my Server and would like to access it via the client, which uses AngularJS. I would like to be able to still use libraries like Restangular or the $resource from Angular. So IMHO the best way to achieve this is to extend the $http service, which is used by most libraries as the basis.
I would like this new service to be able to gracefully fall back to the normal $http, when there is no socket connection available.
In Pseudocode:
socketHttpService = function(config) {
if(socketEnabled) {
var message = buildMessageFromConfig();
primus.write(message);
return promise;
}
return $http(config);
}
Call it like you would $http
socketHttpService({method: 'GET', url: '/someUrl'}).then(function() {
// do whatever
});
My question is, how can i replace the standard $http service with this newly created one? Is there an elegant way, while still retaining the default $http behaviour?
In the meantime, I found a solution to the problem
.config(function($provide) {
$provide.decorator('$httpBackend', function($delegate, $q, $log, SocketService) {
// do not blast mock $httpBackend if it exists
if (angular.isDefined(angular.mock)) {
return $delegate;
}
var httpBackendSocket = function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
if(SocketService.isOpen) {
console.log('open');
method = method.toLowerCase();
// we only know get, post, put, delete
if(method === 'get' || method === 'post' || method === 'put' || method === 'delete') {
// we can not handle the authentication links via sockets, so exclude them
if( url.substring( 0, '/api/v1/currentuser'.length ) !== '/api/v1/currentuser' &&
!angular.equals(url, '/api/v1/login') &&
!angular.equals(url, '/api/v1/logout') &&
!angular.equals(url, '/api/v1/session') ) {
var promise = SocketService.writeRest(method, url, post || {});
return promise.then(function promiseSuccess(response) {
return callback(response.status, response.data, response.headers, '');
}, function promiseError(response) {
// is caught via http handlers
// LATER: If error, retry with $httpBackend ($delegate)
return callback(response.status, response.data, response.headers, '');
});
}
}
}
return $delegate(method, url, post, callback, headers, timeout, withCredentials, responseType);
}
return httpBackendSocket;
});
})
Why? Because it feels like 5 times faster than http, because there is a standing connection and I am not losing any of the realtime options. It's like a cherry on top.
Kind Regards