15

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.

systempuntoout
  • 71,966
  • 47
  • 171
  • 241
  • Would modifying whatever is causing that template to load be an option? You could try to add the template to the `$templateCache` yourself and swallow the error accordingly. – Esteban Felix Nov 09 '14 at 22:43
  • It's the homepage template and it has to be loaded. – systempuntoout Nov 10 '14 at 00:12
  • Are you using ui-bootstrap ????? – vmontanheiro Nov 10 '14 at 00:43
  • @vcrzy yes, we are using angular ui-bootstrap. – systempuntoout Nov 10 '14 at 08:13
  • The router should prevent that route from being taken in the first place. – a better oliver Nov 10 '14 at 12:37
  • Why are you allowing that route to get executed if user does not have permission for it, so you need to check the permissions on client side as well and not only on server side. – harishr Nov 11 '14 at 08:35
  • @harish simply because client is agnostic from the logic of the server; it does not know how the server works and which resources requires authentication and which not. – systempuntoout Nov 11 '14 at 08:43
  • you can shield yourself from such errors given that you are throwing custom exception from server which you can identify on client... – harishr Nov 11 '14 at 09:53
  • In our Http interceptor we are handling the 401 in `ResponseError`, changing the location to the login page and returning the reject promise. – systempuntoout Nov 11 '14 at 10:06
  • @systempuntoout what do you mean when you say the homepage template has to be loaded? If it gives a 401, there's not much you can do about that. – Jonathan Amend Nov 13 '14 at 21:03
  • 1
    Is downgrading angular an option? before it would have the ignoreErrors set to true, they changed it somewhere in 1.3 iirc. – PiniH Nov 13 '14 at 21:48
  • @PiniH no, it is not an option but thanks to have pointed out that it changed in 1.3 . I thought the problem started to raise when we switched from anonymous template to private template; most likely it was when we switched to 1.3. . – systempuntoout Nov 14 '14 at 07:24
  • @JonathanAmend but it could return a 200 and client does not know it. – systempuntoout Nov 14 '14 at 07:29

3 Answers3

14

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;
  }]);
}]);
PiniH
  • 1,901
  • 13
  • 20
  • it looks like an interesting approach, I will take a look at it. – systempuntoout Nov 14 '14 at 07:29
  • 1
    This breaks animation for my app. Commenting this decoraator out lets ngMaterial (thus ngAnimate) render correct classes for animation. Any ideas why? – Dmitry Sadakov Jan 03 '15 at 14:27
  • It could be that they added some new functions to the template request, did you try to debug and see what the original was? if there are any more methods you should override/retarget them to the new decorator. – PiniH Jan 04 '15 at 12:50
  • 'handleRequestFn.totalPendingRequests = 0;' I think ngAnimate waits for totalPendingRequests to hit 0, but because of the wrapping this gets imbalanced. you can try to read the $templateRequest code, it's not long and figure out what needs to be done. – PiniH Jan 04 '15 at 21:00
  • I'm getting errors after compile with this function. Any ideas? Need I inject $templateRequest ? – William Weckl Nov 18 '15 at 10:30
  • Found it! Edited the answer to inject $delegate. – William Weckl Nov 18 '15 at 10:42
  • Now I'm having issues with ng-animate using this code, any clues? – William Weckl Dec 01 '15 at 19:32
  • Worked like a charm, I had to place the config in multiple apps. – Valera Tumash Aug 23 '18 at 05:44
0

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);

});
Dylan
  • 4,703
  • 1
  • 20
  • 23
0

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.

Community
  • 1
  • 1
Jose Ch.
  • 3,856
  • 1
  • 20
  • 34
  • I believe the only working solution could be the option b but we are trying to avoid the anonymous access even for the html markup. – systempuntoout Nov 11 '14 at 14:53
  • Iam using [silhouette](https://github.com/mohiva/play-silhouette) for my scala client backend to protect any html markup using secured and user aware actions. Maybe there is something similar for your ASP.NET "backend". – OliverKK Nov 11 '14 at 15:05