Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NavigationCancel event thrown for valid guards in Angular 6

I'm having issues with the Angular router during the process of navigating to a provided state.

I have tried using a custom guard with canLoad() and canActivate() functions returning booleans as true without any luck.

The Angular docs states the following:

NavigationCancel: An event triggered when navigation is canceled. This is due to a Route Guard returning false during navigation.

As I dont get any more debugging information from the Router tracing (no reason is given) I am forced to check here if there is a fix, or if this is an existing bug. I also appreciate any information regarding other means of debugging the router in Angular 6.

Console output

console output

I've created a small project in here. The project requires access to a provider, I'm using the the provided OpenID Connect provider given in the repository of angular-oauth2-oidc. Password/username is max/geheim.

How to reproduce the error

  1. Clone the repo and serve the site at localhost:4200
  2. Go to localhost:4200/oversikt/index
  3. Login with max/geheim as username/password
  4. Read console

UPDATE I suspect it has something to do with navigating to children: [] routes.

like image 646
Gjert Avatar asked Oct 10 '18 20:10

Gjert


1 Answers

I did a little bit of debugging in your code, and I have found that is not because of links, auth guard, navigation declaration or module configuration nor router beeing flakey in AG6, but it is because of..... OAuth lib you are using.

But let me explain. I found out that following scenario is happening:

  1. You login and beeing redirected back to page
  2. You come back to the application with link like /#YOUR AUT TOKEN
  3. First navigation is scheduled (id 1)
  4. Navigation results in redirection to subpage /oversikt
  5. Navigation 2 pops up (redirection)
  6. Router performs all operations in async way - it is they are serializedbut stilluses RxJS hard- so actual processing is delayed - it has passed current navigation id as argument (that is important)
  7. Some listeners and other subscriptions fires as well as stuff from your OAUTH lib that you are using
  8. It (lib) detects that you have token in your url and REMOVES IT
  9. That triggers another redirection - routing id is INCREMENTED RIGHT AWAY to 3
  10. Now navigation 2 continues and at some point (remember JS is single threaded) it checks (well id does that A LOT) if navigation id from the beginning of the navigation changed or is still the same.
  11. Since it has changes - id 2 was passed at schedule time, and current navigation is 3, single false is propagated all over whole pipes and subscription in router
  12. Having boolean (not even false, but bool) as a value in pipe results in CANCEL navigation without any reason.

Ill add some code references. But in general, that is what happening. Your oauth lib modifies url during navigation and that causes it to be canceled. Guards had like nothing to do with that in straight forward way.

So in general - it is not canceled because "access is denied" like in case of guards, but it is canceled because new navigation will have to be performed, so it is short-circuited by cancel.

Here is (not all) related code:

OAuth lib modifying

   if (!this.oidc) {
            this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
            if (this.clearHashAfterLogin && !options.preventClearHashAfterLogin) {
                location.hash = '';
            }
            return Promise.resolve();
        }

Nav triggering on url change:

 Router.prototype.setUpLocationChangeListener = function () {
        var _this = this;
        // Don't need to use Zone.wrap any more, because zone.js
        // already patch onPopState, so location change callback will
        // run into ngZone
        if (!this.locationSubscription) {
            this.locationSubscription = this.location.subscribe(function (change) {
                var rawUrlTree = _this.parseUrl(change['url']);
                var source = change['type'] === 'popstate' ? 'popstate' : 'hashchange';
                if(this.rou)
                var state = change.state && change.state.navigationId ?
                    { navigationId: change.state.navigationId } :
                    null;
                setTimeout(function () { console.error("FROM LOCATION SUB");_this.scheduleNavigation(rawUrlTree, source, state, { replaceUrl: true }); }, 0);
            });
        }
    };

Nav id modification - happens right away:

   var id = ++this.navigationId;
    console.error("ANOTHER SCHEDULED LOL LOL LOL!!!");
    this.navigations.next({ id: id, source: source, state: state, rawUrl: rawUrl, extras: extras, resolve: resolve, reject: reject, promise: promise });
    // Make sure that the error is propagated even though `processNavigations` catch
    // handler does not rethrow
    return promise.catch(function (e) { return Promise.reject(e); });

That is what is passed to router to start "async" routing - id is nav id (incremented previously)

 Router.prototype.runNavigate = function (url, rawUrl, skipLocationChange, replaceUrl, id, precreatedState) {

This check (it in runNav) fails as first as id changed so 2!==3 - FALSE is returned to the pipe

var preactivationCheckGuards$ = preactivationSetup$.pipe(mergeMap(function (p) {
            if (typeof p === 'boolean' || _this.navigationId !== id) //NAVIGATION ID CHANGES HERE!
            {
              console.warn("PREACTIVATE GUARD CHECK ");
              console.log(p);
              // debugger;
              return of(false);
            }

there are couple of more subscriptions in chaing, all of them have some piped mappings etc as well as known condition check.

 var preactivationResolveData$ = preactivationCheckGuards$.pipe(mergeMap(function (p) {
                if (typeof p === 'boolean' || _this.navigationId !== id)
                    return of(false);

Notice that is what I wrote earlier, that if you get ANY booean here, false is pushed forward. Since we have false here already beacause check failed in previous pipe-map....

Finally at the end of the chain

    if (typeof p === 'boolean' || !p.shouldActivate || id !== _this.navigationId || !p.state) {
      // debugger;
        navigationIsSuccessful = false;
        return;
    }

result flag set to false and this results in

 .then(function () {
        if (navigationIsSuccessful) {
            _this.navigated = true;
            _this.lastSuccessfulId = id;
            _this.events
                .next(new NavigationEnd(id, _this.serializeUrl(url), _this.serializeUrl(_this.currentUrlTree)));
            resolvePromise(true);
        }
        else {
            _this.resetUrlToCurrentUrlTree();
            _this.events
                .next(new NavigationCancel(id, _this.serializeUrl(url), ''));
            resolvePromise(false);
        }

NavigationCancel without any message (last param is the message - emtpy string here) that you know well :).

It took me much more than it should as I didn't know angular internals + those bloody pipes... pipes everywhere.

as for docs

NavigationCancel: An event triggered when navigation is canceled. This is due to a Route Guard returning false during navigation.

well they forgot to mention that internally router can cancel navigation is there is queue of navigaations building up :)

Cheers!

like image 197
Antoniossss Avatar answered Oct 27 '22 01:10

Antoniossss