Angular Guards priority trap

Angular has a great tool for restricting navigation between pages out of the box. But, like any big project, it has its pitfalls. Today I will tell you about one of them.
Refresh your knowledge before reading this article Angular Guards (hereinafter guards). The article will be about CanActivate
, but this applies to the rest of the guards as well. Now let’s move on to the history of priority discovery.
First try
I recently developed a new feature for admins. It was necessary to restrict access to the page – let the user in if he has a certain feature flag and he is an administrator, otherwise redirect to home.
It seems like a trivial task. A solution immediately appeared in my brain with the help of a guard CanActivate
. Suppose there are already two services for these two conditions. Next, we call the necessary methods, combine them into one Observable
and further if at least one returned false
then redirect to the main page if both true
then let the user access the page.
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
private readonly featureToggleService: FeatureToggleService,
private readonly userService: UserService,
private readonly router: Router
) {}
canActivate(): Observable<boolean | UrlTree> {
const isAdminPageEnabled$ =
this.featureToggleService.isAdminPageEnabled$.pipe(startWith(null));
const isAdmin$ = this.userService.isAdmin$.pipe(startWith(null));
return combineLatest([isAdminPageEnabled$, isAdmin$]).pipe(
first(
conditions =>
conditions.some(condition => condition === false) ||
conditions.every(Boolean)
),
map(
([isAdminPageEnabled, isAdmin]) =>
(isAdminPageEnabled && isAdmin) || this.router.createUrlTree(['/'])
)
);
}
}
The code works, but if you look closely, you can write better. The implementation has too much responsibility for one small guard, and the code is not readable. Let’s fix this.
Refactoring
Since there is too much responsibility, you can divide the guards into two, given that the property CanActivate
at the route accepts array of guards.
// admin-page-feature.guard.ts
@Injectable()
export class AdminPageFeatureGuard implements CanActivate {
constructor(
private readonly featureToggleService: FeatureToggleService,
private readonly router: Router
) {}
canActivate(): Observable<boolean | UrlTree> {
return this.featureToggleService.isAdminPageEnabled$.pipe(
map(
isAdminPageEnabled =>
isAdminPageEnabled || this.router.createUrlTree(['/'])
)
);
}
}
// admin.guard.ts
@Injectable()
export class AdminGuard implements CanActivate {
constructor(
private readonly userService: UserService,
private readonly router: Router
) {}
canActivate(): Observable<boolean | UrlTree> {
return this.userService.isAdmin$.pipe(
map(isAdmin => isAdmin || this.router.createUrlTree(['/']))
);
}
}
Now there are two guards that can be reused, and the code has become readable. It would seem that everything is fine, but at this moment I was puzzled by one question. Guards in an array CanActivate
run in series or in parallel?
const routes: Routes = [
{
path: 'admin',
component: AdminPageComponent,
canActivate: [AdminGuard, AdminPageFeatureGuard],
},
];
Things are bad for me if the guards are executed sequentially, since there is no point in forcing the user to wait for each guard separately.
I did some research and found out that they run in parallel, but with some nuances.
Guard Priorities
In the distant seventh version of Angular, priority for guards was introduced. Passed to array for CanActivate
the guards will run in parallel, but the router will wait for the higher priority to complete before moving on. Priority is determined by the order in the array СanActivate
.
Let’s look at an example. There are two guards HighPriorityGuard
and LowPriorityGuard
. Both return Observable
canActivate: [HighPriorityGuard, LowPriorityGuard]
Facts about performing guards:
Run in parallel.
If a
LowPriorityGuard
returnsUrlTree|false
first, then the router will still wait for the executionHighPriorityGuard
before starting navigation.If a
HighPriorityGuard
returns firstUrlTree
it will immediately navigate toUrlTree
fromHighPriorityGuard
.If a
LowPriorityGuard
returnsUrlTree
first and thenHighPriorityGuard
returnsUrlTree
then navigation will take place onUrlTree
fromHighPriorityGuard
since the higher priority wins.If a
HighPriorityGuard
returnstrue
first, the router will waitLowPriorityGuard
because it cannot navigate until it is sure that all guards returntrue
.
If you have questions about guard priorities, please go to this linkI prepared a small demo, it will be easier to figure it out there.
Why priorities are needed
The Anuglar team has added priority for guards to solve the problem of multiple redirects. Let’s say the application has two guards – authentication and admin. The authentication guard will send you to the login page if the user is not logged in. And the admin guard will send to the “You are not an admin” page if the user does not have the required role. Prior to version 7 of Angular, navigation was done from the guard, which was the first to redirect. And it turned out that a user who is not logged in can get to the “You are not an admin” page, because the admin guard was completed faster.
But now we have UrlTree
and the priority of the guards. If the authentication guard is more important, then we put it first in the array, thereby giving it a higher priority, then the router will wait for it to complete and redirect to UrlTree
out of him.
Brief summary
Give guards only one responsibility, then they are easier to read and reuse. But remember the priority trap. If you have the first guard executed in a second, and the second in an hour, and there is no difference in the redirect, then you should think about the order in the array CanActivate/CanActivateChild/CanLoad/CanDeactivate
so that sometimes the user doesn’t have to wait one hour.
I wrote my own solution to bypass the priority of guards, because I was categorically not satisfied with making the user wait, but more on that in the next article.