angular: Render 404 page with CanActivate guard without aborting navigation

I’m submitting a … (check one with “x”)

[ ] bug report => search github for a similar issue or PR before submitting
[X] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Background When a user navigates to a route that requires authentication and they are not authenticated, there’s no way to use a CanActivate guard to render a 404 component and still update the url to what the user was trying to access.

Current behavior Let’s say a user navigates to /protected and is not authenticated. Currently, in the CanActivate guard, in order to render my 404 component, I would use router.navigate(['404'], { skipLocationChange: true }); return false;. However, after rendering the 404 component, the url in the address bar is the previous url before the navigation since the navigation was aborted.

Expected behavior In CanActivate guard, there should be a way to render the 404 route while still updating the url in the address bar to /protected.

Minimal reproduction of the problem with instructions

@Injectable()
export class LoggedInGuard implements CanActivate {
  constructor(private router: Router, private userService: UserService) {}

  canActivate() {
    if (this.userService.isLoggedIn) return true;

    this.router.navigate(['/404'], { skipLocationChange: true });
    return false;
  }
}

This code will render a 404 page, but not update the url. I would like it to render 404 and update the url in the address bar to what the user was trying to access.

What is the motivation / use case for changing the behavior? Github does this, for example, if you go to https://github.com/simonxca/webpack-guide, it will render a 404 page even though the repo exists because you’re not authenticated. The motivation is to prevent users from being able to distinguish between a protected from a non-existent page for security.

Please tell us about your environment: Mac OSX Sierra, Node 6.10.3

  • Angular version: 4.1.X

  • Browser: all

  • Language: all

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 76
  • Comments: 16 (4 by maintainers)

Commits related to this issue

Most upvoted comments

Are there any updates on this? This seems like it would be a common request, but I cannot find much on it.

The use case for me is to have pages only certain users can access. I already don’t render the pages in the navigation bar when they don’t have them, but when they manually navigate to the certain pages, I want it to act as if the page does not exist, even though it does. Right now, I got it showing the 404, but the URL just goes back to the root.

Imperfect workaround for anyone else landing here. You still get a small blink of the previous route since you’re returning canActivate false and then the url is replaced.

In my case this was good enough.

  • The components route (/no-access in this example) is never in the history due to skipLocationChange: true
  • Customers will see the intended route in the URL, but the contents of the /no-access component instead

location.replaceState docs

Guard


import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, CanActivate } from '@angular/router';

@Injectable()
export class MyAccessGuard implements CanActivate {
  constructor(
    private router: Router
  ) { }


  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    const hasAccess = hasAccess();

    if (hasAccess) {
      return true;
    }

    this.router.navigate(['/no-access'], {
        skipLocationChange: true, // minimal effect. see https://github.com/angular/angular/issues/17004
        queryParams: {
            url: state.url
        }
    });
    return false;
  }
}

Receiving Component for /no-access route


import { Component, OnInit } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-no-access',
  templateUrl: 'no-access.component.html',
  styleUrls: [
    'no-access.component.scss'
  ]
})

export class NoAccessComponent implements OnInit {
  constructor(
    private activatedRoute: ActivatedRoute,
    private location: Location
  ) { }

  ngOnInit() {
    const replaceUrl = this.activatedRoute.snapshot.queryParams['url'];
    if (replaceUrl) {
        this.location.replaceState(replaceUrl);
    }
  }
}

Would be helpful if this were possible in other guards as well – for example I have a Resolve guard which does a server lookup for route data, which can result in a 404 or authentication error as well, and I’d like to be able to display a 404 or 401 error without aborting navigation.

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<MyData>
  {
    return this.myService.getRoute('/' + route.url.join(''))
      .map(myData => {
        if (myData) {
          return myData;
        }
        this.router.navigate(['/NotFound'], { skipLocationChange: true });
        return null;
      })
      .catch((error: MyError) => {
        if (error.status >= 400 && error.status < 500) {
          this.router.navigate(['/NotFound'], { skipLocationChange: true });
        } else {
          this.router.navigate(['/ServerError'], { skipLocationChange: true });
        }
        return Observable.of(null);
      });
  }

Is there any update on this? Since it’s a very common case for all projects.

This is related with #16981. A workarround could be the proposed by @ccondrup on this comment https://github.com/angular/angular/issues/16981#issuecomment-549330207.

@JackMorrissey’s workaround does the trick, but using location.replaceState breaks back navigation as no new state had been pushed to the History stack since the guard returned true. Using location.go (which is basically just pushState) solves that. The URL still blinks when no route had been previously activated though.

My Brothers and my Sisters I have solution about this.

When you said “false” to canActivate, you told the Router, “do not change the Url”.

If you need to keep that Url, canActivate is not the right tool for you.

You can use a route Resolve. Inside the Resolve, return either your object you want, or undefined.

Now let your component load. The one that requires the object from the Resolve.

Inside that component you gonna have an onInit who is listening to ActivatedRoute.data.

If you found your object you’re looking for inside ActivatedRoute.data, your component can do it’s thing.

Now the best part, in your component who is already loaded, when you found out your ActivatedRoute.data didn’t have your object because the Resolver set it to undefined. Now you can just use router.navigate to your 404 with skipLocationChange true.

You permitted navigation, because you wanted the Url to stay. But you show the 404 route silently. Your component needs to know how to do that.