How to avoid $compile:tpload errors on 401 status code response - javascript

We are developing a Single Page Application with AngularJS and ASP.NET MVC Json Rest API.
When an unauthenticated client tries to navigate to a private route (Ex: /Foo/Home/Template) to get a template, it gets a 401 response from the Web API and our AngularJS app automatically redirects it to the login page.
We are handling the 401 with $http interceptor with something like this:
if (response.status === 401) {
$location.path(routeToLogin);
return $q.reject(response);
}
Entering the correct credentials allows the client to get the template.
Everything is working perfectly except for one detail; the Javascript console reports this error:
Error: [$compile:tpload] http://errors.angularjs.org/1.3.0/$compile/tpload?p0=%Foo%2FHome%2FTemplate%2F
AngularJs documentation states this:
Description
This error occurs when $compile attempts to fetch a template from some
URL, and the request fails.
In our AngularJs app the request fails but it is by design because the resource is there but it cannot be accessed (401).
Should I move on and accept this kind of error on console or is it possible to mute or shield it in some way?
EDIT:
I have debugged the angular source a little bit and I found what part of the code is raising the exception.
Since we are using TemplateUrl to declare our templates, we are indirectly using the function compileTemplateUrl that makes this call:
$templateRequest($sce.getTrustedResourceUrl(templateUrl))
this leaves the second parameter (ignoreRequestError) of templateRequest undefined.
ignoreRequestError(optional)boolean
Whether or not to ignore the exception when the request fails or the
template is empty
When our http interceptor, handling the 401 status code, rejects the promise, the $http.get inside the $TemplateRequestProvider fails and calls this function:
function handleError() {
self.totalPendingRequests--;
if (!ignoreRequestError) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
}
return $q.reject();
}
I believe we can't do anything to prevent the error on console as TemplateUrl does not allow to set the ignoreRequestError flag to false.
I've tried to bypass the reject in case of 401 status code; this fixes the error on console but sadly it has a side effect: an empty template is wrongly cached into the TemplateCache causing othe problems.

After some thinking I remembered about decorating in Angular, it solved this problem perfectly:
app.config(['$provide', function($provide) {
$provide.decorator('$templateRequest', ['$delegate', function($delegate) {
var fn = $delegate;
$delegate = function(tpl) {
for (var key in fn) {
$delegate[key] = fn[key];
}
return fn.apply(this, [tpl, true]);
};
return $delegate;
}]);
}]);

You should be able to intercept the call for the template by status and url.
Plunker
app.config(function($httpProvider) {
var interceptor = function($location, $log, $q) {
function success(response) {
// The response if complete
$log.info(response);
return response;
}
function error(response) {
// The request if errors
$log.error(response);
return $q.reject(response);
}
return function(promise) {
return promise.then(success, error);
}
}
$httpProvider.responseInterceptors.push(interceptor);
});

As I see it, you have two options:
Option A)
go with the interceptors. However, to eliminate the compile you need to return success status code inside response error (BAD) OR redirect to the login page inside the interceptor (Good):
app.factory('authInterceptorService', function () {
var interceptor = {};
interceptor.responseError = function (rejection) {
if (rejection.status === 401 && rejection.config.url === "home template url") {
//BAD IDEA
//console.log("faking home template");
//rejection.status = 200;
//rejection.data = "<h1>should log in to the application first</h1>";
//GOOD IDEA
window.location = "/login.html";
}
return rejection;
}
return interceptor;
});
and on app config:
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('authInterceptorService');
}
Option b)
make the home template public. After all it should be just html mark-up, without any sensible information.
this solution is clean...and perhaps is also possible.

Related

Angular ngResource $save Method Clears $resource Object

Using Angular 1.5.5 here:
Is there any way to tell Angular to ignore response body for particular requests (such as $save)? It drives me crazy that after I call $save, angular updates the model with the object returned by a server, which initially was supposed to be used to distinguish between different resolutions of the request. It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
In case you need more info, relevant code is below.
Controller:
'use strict';
angular
.module('app.operators')
.controller('OperatorNewController', OperatorNewController);
OperatorNewController.$inject = ['operatorsService', 'notify'];
function OperatorNewController(operatorsService, notify) {
var vm = this;
vm.done = done;
activate();
function activate() {
vm.operator = new operatorsService();
}
function done(form) {
if (form.$invalid) {
// do stuff
return false;
}
vm.operator.$save(function(response) {
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
}
}
Service:
'use strict';
angular
.module('app.operators')
.service('operatorsService', operatorsService);
operatorsService.$inject = ['$resource'];
function operatorsService($resource) {
return $resource('/operators/:id/', {id: '#_id'}, {
'update': { method: 'PUT' }
});
}
Server request handler is also fairly simple:
.post('/', function (req, res) {
if (!req.operator.active) {
return res.status(500).json({ inactive: true, success: false });
}
// do stuff
return res.json({ success: true });
});
In either way I don't like the idea of having to send the entire object from server (particularily when it's a failed request), and even if I have to, I still need a way to send some extra data that will be ignored by Angular.
Your help is very much appreciated!
The $save method of the resource object empties and replaces the object with the results of the XHR POST results. To avoid this, use the .save method of the operatorsService:
//vm.operator.$save(function(response) {
vm.newOperator = operatorsService.save(vm.operator, function(response),
if (response.success && response._id) {
$state.go('app.operators.details', {id: response._id}, { reload: true });
} else if (response.inactive) {
// do stuff
} else {
// do other stuff
}
}, function (error) {
// do other stuff
});
UPDATE
It results in unwanted form clear. Interestingly enough, this behaviour remains even if I send a 400 or 500 http status code.
This behavior is NOT VERIFIED.
I created a PLNKR to attempt to verify this behavior and found that the $save method does not replace the resource object if the server returns a status of 400 or 500. However it does empty and replace the resource object if the XHR status code is 200 (OK).
The DEMO on PLNKR
It drives me crazy that after I call $save, angular updates the model with the object returned by a server
It helps to understand how browsers handle traditional submits from forms.
The default operation for a submit button uses method=get. The browser appends the form inputs to the URL as query parameters and executes an HTTP GET operation with that URL. The browser then clears the window or frame and loads the results from the server.
The default operation for method=post is to serializes the inputs and place them in the body of an HTTP POST. The browser then clears the window or frame and loads the results from the server.
In AngularJS the form directive cancels the browser default operation and executes the Angular Expression set by either the ng-submit or ng-click directive. All $resource instance methods including $get and $save, empty and replace the resource object with XHR results from the server if the XHR is successful. This is consistent with the way browsers traditionally handle forms.
In RESTful APIs, HTTP GET operations return the state of a server resource without changing it. HTTP POST operations add a new resource state to the server. APIs usually return the new resource state, with additional information such as ID, Location, timestamps, etc. Some RESTful APIs return a redirect (status 302 or 303) in which case browsers transparently do an HTTP GET using the new location. (This helps to Solve the Double Submission Problem.)
When designing RESTful APIs, it is important to understand how traditional browsers behave and the expectations of RESTful clients such as AngularJS ngResource.

In angular make $http go to catch if server response with {error:'ok'}

$http.get('/data.json')
.then(function(){console.log('ok'})
.catch(function(){console.log('no ok')})
The server response is:
200 OK
content-type: application/json
{error:'cannot get the data'}
I want that the response will go to the .catch not to .then.
I know that I can change the response header from the server, but I want to do it only on the client side.
In other Words:
How I make angular $http promise service, to think that 200 OK status, with "error" key in the response object, will go to catch instead of invoking the then function?
You can use an interceptor:
yourApp.factory('myInterceptor', ['$q', function($q) {
return {
response: function(response) {
if (response.status === 200 && response.data.error) {
return $q.reject(response);
}
else {
return response;
}
}
};
}]);
$httpProvider.interceptors.push('myInterceptor');
$http.get('/data.json')
.then(function(res){
if(res.error === 'cannot get the data'){
return $q.reject(res)
}
return res;
)
.then(function(){console.log('ok'})
.catch(function(){
console.log('no ok')
})
Just as others suggested, you can check for the conditions you want the request to be treated as a failure inside the .then block and reject using angular $q service reject() function
As #yarons pointed out, you could use an interceptor. But, your decision was to always return 200, even in an error case, so why do you want to change this behaviour in your Front End now?
Your logic seems like:
Don't tell the front end to throw an error (maybe to not show in the
dev console or let the user now), but handle it internally as an
error.
For me, if you decide to go for this trick-behaviour, go all the way and don't hack something around. Just look in the then() for the error message.
So go in the then(), as you planned, and then catch your error there with an if clause:
$http.get('/data.json')
.then(function(response){
if(response.data.error) {
$scope.error_message = response.data.error;
return;
}
});

Angular JSON-RPC: processing error

I am trying to improve an AngularJS service for invoking remote JSON-RPC services. In accordance with the JSON-RPC specification, when an exception occurs at the server side, the response should include an error object with the description of the same:
response = {
jsonrpc: "2.0",
result: null,
error: "Description of the error",
id: 1,
}
... where "id" is the identifier of the request original sent to the server.
The Angular component that I am trying to improve (https://github.com/ajsd/angular-jsonrpc) processes the responses from the server using $http transformers. I modified the original transformer, so that the new one looks like this:
transforms.push(function(data) {
//Original code: return data.id === id ? data.result || data.error : null;
if (data.error !== null) {
throw data.error;
}
if (data.id !== id) {
throw '[jsonrpc] Wrong response ID, id = ' + data.id;
}
return data.result;
});
As you can see, I throw an exception with the description of the error; which is a really poor solution since this service is based on $http promises and, therefore, the invoker of the service will find it difficult to catch the exception.
How can I invoke from within the $http transformer the "error" promise that the user originally sets while invoking the $http service?
$http.get(url).then(success(data) {...}, error(data) {...}};
Is this a correct approach or should I better base my modification on interceptors?
if you left the code as you had it originally, you can define a service which uses the jsonrpc module:
angular.module('myApp').
service('jsonrpcservice', function(jsonrpc) {
var service = jsonrpc.newService('svc');
this.get = service.createMethod('getData');
});
..in your controller somewhere:
jsonrpcservice.get({params}).success(function(result){}).error(function(err){});
you can handle the error in the .error() returned by $http

Angular $http.jsonp or get returning 404 on a mobile

I am pulling in JSON from a file I am storing locally. When I view this in the browser it works, when I view it in an Ionic packaged app it returns 404
I am using the following code:
.factory('cardFactory', function ($q, $http, $rootScope) {
return {
getCards: function () {
var deferred = $q.defer(),
httpPromise = $http.jsonp('/static/cards.json');
httpPromise.then(function (response) {
deferred.resolve(response);
}, function (error) {
console.error(error);
});
return deferred.promise;
}
};
});
And calling it like so:
cardFactory.getCards()
.then(cardSuccess, cardError);
I have tried with GET instead of JSONP, both return a 404 response.
I am aware of the access-control-allow-origin issue, but surely the jsonP should solve that?
This is at the same level (hierarchically) as my images, which are served fine.
Any ideas what's going on?
The solution was to change the request back to a simple GET, and lose the first slash as so:
var httpPromise = $http.get('static/cards.json');

Angular - Loading a view without changing the url

I'm working on a project which has several views, which some of the views may not be accessible to a given user. What I want to do is that, if a user navigates to a url that is restricted (due to his permissions), lets say '/project/manhatten/security' I want to display a view (an html partial) which simply says 'Access Denied'.
But I want to display the view without changing the url. I want the url to stay '/project/manhatten/security', so the user can copy the url and give it to someone with enough permission, and it would work fine.
What is the best way to achieve this ? Can I still use ng-view or a combination of ng-view and ng-include ?
Thanks in Advance.
I don't know of a way on how to restrict access to a specific view in angular. I think you shouldn't restrict views. What you should do is restrict access to your api. So if a user doesn't have the privilege to delete a project. Simply send a 401 from the server when he calls the api. On the client side handle this 401 in angular with an $http interceptor.
I would do the following:
In your index.html create an element/directive to display the error message
index.html
<div ng-controller=ErrorMessageCtrl ng-show=error.message>
{{error.message}}
<ng-view></ng-view>
The ErrorMessageCtrl will get notified when an access denied error occured:
.controller('ErrorMessageCtrl', function ($scope) {
$scope.error = {}
$scope.$on('error:accessDenied', function(event, message) {
$scope.error.message = message
})
})
Create an interceptor service to handle http 401 auth error:
.factory('authErrorInterceptor', function ($q, $rootScope) {
return {
response: function (response) {
return response
},
responseError: function(rejection) {
if (rejection.status === 401) {
$rootScope.$broadcast('error:accessDenied', 'Access Denied')
}
return $q.reject(rejection)
}
}
})
add the interceptor service to $httpProvider
.config(function ($httpProvider, $routeProvider) {
$httpProvider.interceptors.push('authErrorInterceptor')
$routeProvider.when('/project/manhatten/security', {
template: '<div><h1>Secure Page</h1>secure data from server: {{data}}</div>',
controller: 'SecureDataCtrl'
})
})
$http.get('/api/some/secure/data') returns a 401 http status code
.controller('SecureDataCtrl', function ($scope, $http) {
$scope.data = 'none'
$http.get('/project/manhatten/security')
.success(function (data) {
$scope.data = 'secure data'
})
})
Please keep in mind that this was hacked in 10 minutes. You need to do some refactoring. Like creating an errorMessage directive which gets the error notification service injected and not broadcasting the error message to the scope.

Categories

Resources