canActivate=>false can result in blank screen/dead link #16211
Comments
|
Yep, this also happens to me. |
|
@matthewerwin How would you handle a use-case when you actually need to cancel routing after
|
|
This also resolves #14515 |
|
Issue seems is related to #18975 |
|
I'm surprised more people haven't come across this yet... Seems to be a pretty big flaw if there's no decent solution. Is it possible to detect if this is the first route being navigated to (i.e. on page load)? Could then redirect if so, else simply return false. |
|
This seems to be working for me: if (this.router.routerState.snapshot.url === '')
this.router.navigate(['/'])I assume |
|
Hi , I too have the same issue . When canactivate returns false, the route doesn't stay with the previous one, instead goes blank ..like http://localhost:1000/a1 I my case i dont want to return to the "/" . I just need to stay on the last route navigated . ie http://localhost:1000/a1(with no refresh) |
|
I really didn't get why is the return type of canActivate boolean, observable and not - for example boolean | RouterCommands or observable<boolean | routercommands> so you would just: |
|
any updates here? i am using the latest version of angular 4. |
|
@jasonaden what's the word? What do you need from me? Anyone else on this thread--if you need a build for a particular version of Angular let me know and I'll get it posted to MyGet feed so you can pull in. |
|
Hi , Any solution for this issue ? or will this be taken care in the next updates? The issues comes in when the user changes the url from the browser url and tries navigating to a page which he is not allowed to , because of the canactivate condition returning false; If any alternative , please suggest. |
|
It would be great if we could return for example: Or... some other behavior similar to that. |
|
@Goodwine that should be supported within my version. Please feel free to verify by installing and checking you use case -- returning false on all routes will result in dead link, reject or observable error results in error, and support for ** default route: npm uninstall @angular/router |
|
@Goodwine sorry, sent that too soon. The idea of having strings doesn't make a lot of sense to me. CanActivate guards are sentinel, not dynamic route builders. if you want that to be the fallback you would add next in the route table and if it needed conditionality just create yet another CanActivate. In essence what you're asking for is supported behavior in my code but not in the way you're trying to do it which would muddy domains of responsibility |
|
This might be something fixed with a recent update in #20803. Can you confirm? |
|
@jasonaden no, that one does not fix the issue and doesn't allow fall through behavior on the routing table |
|
I am now using the latest Angular version 5+ . Seems like the issue is not solved there as well. Any idea when this will be taken care of ? |
|
Hi all, try below code... setTimeout
|
|
@micsel sory, but this is not the solution. With the same result you may call router.navigate() inside the component after needed checks. The logic of code is "try open route" => "canActivate? no!" => "reject component activation" => "navigate where I need programatically". But we don't need to implement own guards because we have them out of box. The only problem of this very issue is that if we open link like a/b/c and "canActivate" of "c" returns false then router rejects all snapshots of chain to be activated. Expected result: we have access to a/b |
|
I'm also seeing this with a |
|
@markgoho -- all of the guards are fixed in the PR. @jasonaden stated that it will be slated for review and inclusion in next few weeks sprint. Routes can all be fully guarded and will flow to the first fully available route. |
|
For anyone that wishes to test the solution I have builds through v6.0.0-beta.4 https://www.myget.org/feed/snaptech/package/npm/@angular/router I haven't really had problems if the versions didn't match up with the rest of the Angular build but hopefully this will be folded in and released in 5.2.5 and absolutely for v6 |
|
I just solve my issue in my previous comment, it end up like this
where UrlExistGuard checks wether :slug is a valid category or a valid product slug, |
|
This is still an issue; it doesn't seem that there are really any solid workarounds. Any updates on a fix? |
|
@cgosiak go upvote the PR (#16416). @namdien177 if your routing module has to be aware of where to go next (if guard fails) it defeats the entire purpose of a router. Further, it makes it impossible to have independent modules that are lazy loaded. @jhonland that doesn't allow you to have a product module and a category module & further requires them to be cross-aware. The redirect also has a bunch of other problems to it. My request is that everyone upvote the PR (thumbs up, etc) so we can get some prioritization from Google team. Because this is a medium risk (it effects a core part of Angular) they have to be cautious & that caution causes it to keep getting pushed off for merge for other things with higher priority and lower support risk. Go upvote the PR. 8.0.0 is about to release so this is a perfect time to get this merged. |
|
An ugly workaround is to mutate your route config at runtime. It even works with AOT For example, say you wanted to serve you marketing site and your user's home page at the root of your domain. const appRoutes: Routes = [{
path: '',
children: [
{
path: '',
canActivate: [MarketingPageGuard],
loadChildren: './pages/marketing/marketing-page.module#MarketingPageModule'
},
{
path: '',
canActivate: [AuthGuard],
loadChildren: './pages/home/home-page.module#HomePageModule'
}
]
}];// canActivate method of MarketingPageGuard
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const canActivate = !this._session.isActive;
if (!canActivate) {
// @ts-ignore
route.parent.routeConfig.children.reverse();
this._router.navigate(['/']);
}
return canActivate;
} |
|
I faced the same problem in Angular 8.0.3 and found a one-liner-solution that at least works for me: For the impatient:export class MyGuard implements CanActivate {
constructor(private router: Router) {
}
canActivate(): boolean {
const isConditionFulfilled = ... // my condition
if (!isConditionFulfilled) {
// notify user in some way:
alert('You shall not pass!');
// My magic line:
this.router.navigate(<any[]><unknown>'');
}
return isConditionFulfilled;
}
}Further information:My expectations/what I wanted to achieve in my application:Component
In other words, I did neither expect nor want the user to be redirected in any case. The relevant part of my Routing module:const routes: Routes = [
// ...
{path: 'condition-based-component', component: ConditionBasedComponent, canActivate: [MyGuard]},
{path: '', component: WelcomeComponent},
{path: '**', redirectTo: ''}
];Additional information:
|
|
I have route guard that returns false when some condition is met as I want user to stay on the current route if condition is not met. |
Place your AuthGuard paths in the bottom rows. I can't explain it, but it works. My guess is that once we trigger the function and it returns false and navigates /Home, we are still using the same Routes list with router-outlet instead of a new instance of the list. Since it is the same list, it might be causing the AuthGuard rows and the rows after to be false. Yes, I know this sounds absurd, but how many times have YOU encountered errors that were completely off-base and literally due to a missing comma or something similar?
It is, but the component isn't loading so you won't see "localhost:4200/Home" on your address bar and will just show "localhost:4200". The html will load from "src/index.html" just fine, but the component itself doesn't activate. Ultimately, defeating the purpose and ruining your day. When I moved the AuthGuard rows to the bottom of the list, everything started to work just fine. const routes: Routes = [
{ path: "", redirectTo: "/Home", pathMatch: "full" },
{ path: "Home", component: HomeComponent },
{ path: "Login", component: LoginComponent },
{ path: "**", redirectTo: "/Home" },
{ path: "User", canActivate: [AuthGuard], component: UserComponent },
{ path: "Admin", canActivate: [AuthGuard], component: AdminComponent }
]; |
That should actually prevent you from opening those routes because the catch-all route will be evaluated first. Did you check whether those still work properly?
That's exactly what this issue is about. Accessing a guarded URL directly breaks the application. |
|
Further to the point is that this PR deals with having AuthGuard1 and AuthGuard2, etc...eg admin vs normal user login and having the route table operate correctly in all scenarios. Moving to the bottom for a single guard type and no need for fall-through in a fail scenario works because it’s a simplistic case. Doesn’t matter there because if the first one fails all the others would fail as well below |
Your logic makes sense in that the catch all would redirect to /Home and never reach the last two paths. However, the point is to prevent users from manually entering the address in the browser to access those pages unless they are already logged in with a token. Not to break the app so they can't do anything. The issue comes on the redirect when manually entering in the path to a page where authentication fails. It loads the page (html) but not the component. Jbojcic1 is having the issue where it loads to "localhost:4000" instead of "localhost:4000/path", as was I. If it loads to a blank, it should redirect home, right? No. Because of base href being set to "/". Technically speaking, "localhost:4000" is really "localhost:4000/" without a path. Your catch all would be triggered on this and complete the redirect. Possible rebuttal: "Well, you could set the navigate without the '/' and it should work." Yes, it should. But it doesn't (and yes, this was also tested). It's not technically a blank path and if our catch all is at the end of the routes list, it's not being triggered to catch the solitary "/". When moving all of the AuthGuard to the bottom, it is now able to catch it and complete the navigation to /Home. In production and hosting on the web, dropping the AuthGuard paths to the bottom most likely won't matter, but localhost has it's own set of problems that need to be worked around to move forward with development. |
In my scenario, I don't require a separate AuthGuard for admin. If there is no token set, they shouldn't be able to access any of those pages (or their children). I use a service that determines whether the login is a user or admin and redirects them to the correct "portal" to prevent users from accessing admin functions. Additionally, my server will prevent users from performing admin functions should they manage to get access to the admin functions in the app. My app is based on integration with business server access so the user can't even log on if they have no server access granted to them. |
|
still seeing this... |
|
It did worked for me, after returning
|
My Temporary hack. |
|
@bugger279 using router.navigate() inside the guard is indeed the way to go right now. I would not call it a hack but a workaround for a bug that has been around for a long time now. |
|
@cmddavid trying to redirect navigating mid-route using navigate only addresses part of the issue in very basic SPA’s. I’m going to explain this in detail because it’s come up a few times in this thread and discussions elsewhere. Consider scenario 1: You have lazy loaded modules. That means your module now has to be aware of any others to know where to switch to (I.e. trying to act like a router itself that’s knowledgeable of the other routes...) A random guard, in a module that requires special permissions to possibly even load, is now trying to make decisions about other modules that may be loaded or not loaded depending on other permissions of the user. Further every module basically is now having some kindve mini alternate routes table...which doesn’t interrupt the current route request. But queues another behind it. Now with potentially every route in the composite table (Angular route table) trigging a call to navigate() trying to be smart about where to send the user. Scenario 2: Multiple generic variable based routes. e.g. the @handle scenario. Where @OrganizationName used module 1 and @userName uses module 2. Now calling navigate() in the first guard will result in an endless route loop to itself. Scenario 3: your app has more than 1 developer. Trying to keep routing straight just went out the window along with dev ta efficiency. Calling navigate() to try to make router level decisions within a guard is a hack. A guard isn’t a router and shouldn't be trying to act like one. Further, the angular router continues executing against the existing mid-flight route when a guard returns false. So calling navigate() just queues another route attempt. Meanwhile the router keeps calling the next series of guards. So then you could have a route that matches successfully just below your guarded route that returns false and navigate() in its infinite wisdom about what to do just because permissions failed. Now your app will then render that 1st successful matching route down the table. Then the failed guard will override that completed navigation with a new route execution. You can see how this also doesn’t work at all if an app has two routes that both defer to each other when a user doesn’t have a certain permission combination for example...meanwhile a route the user would have access to render just below those two in the application’s composite routing table keeps getting overridden by redirecting guards. So then you’re writing switches and permission checks to iterate through where you should redirect the navigation in multiple modules that all have to be aware of each other and may not have been lazy loaded. With every guard in the table potentially adding to the route-stack with a navigate call...that can just trigger the same series. Every guard, for every potential match to a route request, in the composite routing table is executed until the router finds a suitable allowed route (or one that has no guard). On that basis alone of well-known router behavior it can be fundamentally stated that: Calling navigate() from a guard is an anti-pattern. Doing so solves only very rudimentary small app scenarios with fixed unique route combinations, no lazy loaded modules, and no use of variable routes with the same pattern. Forget having multiple devs doing anything on a project together who don’t have awareness of other modules or understand the behavior of the router. Just a recipe for bad runtime issues for the user and for bad security practices when the support team just starts granting permissions to deal with it. I hope that clarifies the matter substantially. I do think it’s helpful to supply workarounds to people as there are plenty of situations where a hack is sufficient. |
|
@matthewerwin thank you for the detailed description of the impact of having to go with workarounds like this. I think all the more reason to get some real progress on this issue. Maybe a solution would be to have the ability to set a routing strategy for if the guard blocks entry to the matched route. Having the ability to go for the next matched route could be sufficient? |
|
@cmddavid the PR to fix it has been available for a long time. The Angular team has been slammed with a lot of work in other areas. You can upvote the PR here: @IgorMinar @atscott a comprehensive overview of what all that PR addresses is outlined above in my last comment on this issue. |
|
Hi @matthewerwin, first and foremost thanks so much for this feature, sorely needed IMHO. I've been using it for a while now up to version 8, however when I updated to the 9.1 rc (or 10) I noticed that it's not working as I expected any longer since the route is getting canceled and doesn't continue onto the next viable route. I've tried with setting the Thankfully version 8 is still working, but I'd like to update to the latest and greatest as it'll be closer to the final api when this hopefully lands soon. Thanks again in advance. |
|
@lmdg I appreciate the kind words. For version 10 you must explicitly use (and use the MyGet upstream feed): For version 9 there are a variety of builds available; I recommend using the latest one I posted (9.1.0-rc.1). You can see a list of all builds I've posted here: https://www.myget.org/feed/snaptech/package/npm/@angular/router As there have been no real router changes of significance by the internal team the builds will work with a majority of the various builds from Angular & this one is backwards compatible. Please note that in order to get it to work you need to enable on v10+ Hope this sorts it out for you. |
I'm submitting a ... (check one with "x")
Current behavior
canActivate, canActivateChild will result in NavigationCancel event if 'false'/reject() happens in the Guard. Additionally the URL is reset to the base path before the navigation attempt (as though to leave the user on the page they navigated from).
This current behavior means that if you just go directly to a location in your browser (e.g. localhost:4200/some-guarded-route) and it returns false the URL will read "localhost:4200/" with a blank page as navigation is canceled.
Expected behavior
The right behavior to me seems that the router should try to see if there are further matches in the routing table (i.e. the guard 'false' case would cover the use case scenario of #14515). By continuing down the route table the behavior would be to land on '**' route in the worst case while also enabling conditional routing.
Minimally NavigationCancel event should determine if the resetUrlToCurrentUrlTree is an already displayed route (either rendering it if necessary or doing nothing if it's already rendered).
I will see if I can issue a PR to fix this for review. Will need to determine what the side effects would be for cases where people would expect just to stay on the currently rendered page (considering it was already rendered). However, to me I think the correct behavior for a dead link should be to go down the route table and hit '**' for a 404 or something vs doing nothing when the user clicks on it and rendering nothing if the user hits the route directly before any route has been rendered.
Minimal reproduction of the problem with instructions
Plunker won't work for this because you would need to be able to hit the route as though it was bookmarked to reproduce.
Effectively just create project using CLI, create a route guard, return false in the methods. Then attempt to hit the route directly without loading a root non-guarded route first.
What is the motivation / use case for changing the behavior?
Motivation is that you won't have the user on a blank screen if they happen to hit a guarded route without meeting the conditions.
Second part is that by resolving these scenarios by continuing down the route table you would enable conditional routing for scenarios such as:
Creating routes like:
localhost:4200/:organization
localhost:4200/:user
If the route guard found a valid organization it could allow the request to move forward vs user. This is all gravy. A start is to not give the user a blank page when resetting the route via resetUrlToCurrentUrlTree when the current Url wasn't rendered in the first place.
Please tell us about your environment:
Mac OS Sierra (10.12.4), WebStorm, NPM, ng serve
Language: TypeScript 2.2.2
Node (for AoT issues):
node --version=Node 7.9
The text was updated successfully, but these errors were encountered: