universal: Guards and Resolvers don't wait for APP_INITIALIZER, if RouterModule initialNavigation: 'enabled' is set

šŸž bug report

Affected Package

@angular/router/RouterModule @angular/core/APP_INITIALIZER

Is this a regression?

Yes, this used to work all the way up until Angular 8.2.14. This bug started to appear once I updated to Angular 9 (9.0.7) EDIT: I updated to Angular 9.1, but the problem persists.

Description

After Upgrading I noticed, that some parts of my application wouldnā€™t start any longer, because the configuration they are depending on is empty. I used to load this configuration from a config file on my server. Iā€™m using APP_INITIALIZER for this. Upon further investigation I found out, that my guards and resolvers run before my APP_INITIALIZER promise resolves. Finally I could pin-point this error to app-routing.module.ts. When I uncomment initialNavigation: 'enabled' then the guards and resolvers wait till the APP_INITIALIZER promise resolve. Once enabled, the guards and resolvers run immediately and donā€™t wait for APP_INITIALIZER.

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
    // initialNavigation: 'enabled'
})
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

šŸ”¬ Minimal Reproduction

Open the console to see when different parts of the application get executed. MinimalGuard and ResolverService should get executed after AppInitializerService Promise resolved, but unfortunately they get executed before.

https://stackblitz.com/github/felixfiskare/ResolverAndGuardBeforeInit

https://github.com/felixfiskare/ResolverAndGuardBeforeInit

šŸ”„ Exception or Error

guardAndResolverBeforeAppInit

šŸŒ Your Environment

Angular Version:

 ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / ā–³ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 9.0.7
Node: 13.11.0
OS: linux x64

Angular: 9.0.7
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.7
@angular-devkit/build-angular     0.900.7
@angular-devkit/build-optimizer   0.900.7
@angular-devkit/build-webpack     0.900.7
@angular-devkit/core              9.0.7
@angular-devkit/schematics        9.0.7
@ngtools/webpack                  9.0.7
@schematics/angular               9.0.7
@schematics/update                0.900.7
rxjs                              6.5.4
typescript                        3.7.5
webpack                           4.41.2

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 12
  • Comments: 22 (2 by maintainers)

Most upvoted comments

@felixfiskare This is caused by the NgUniversal schematics, we added it to avoid flicker when bootstrapping on the client. Weā€™ll move this issue over to angular/universal so that we can track a proper resolution. This may be a warning or a prompt, or possibly a link to documentation about certain pitfalls with adding initialNavigation, like the one you encountered here.

Iā€™ve looked into the docs and the router_module and determined that guards and resolvers being run before the userā€™s own APP_INITIALIZER is working as intended.

  • The router has its own APP_INITIALIZER.
  • All APP_INITIALIZERS are run in parallel.
  • When using initialNavigation: 'enabled' the routerā€™s APP_INITIALIZER triggers a navigation. This means that if your initial navigation depends on your own APP_INITILIZER being completed first, you cannot use initialNavigation: 'enabled'.
  • ApplicationRef.bootstrap (which happens after APP_INITIALIZER completion) calls all APP_BOOTSTRAP_LISTENERs, which triggers initial navigation if using initialNavigation: 'legacy_enabled'
  • All of this also seems to follow the documented behavior for initialNavigation:

ā€˜enabledā€™ - The initial navigation starts before the root component is created. The bootstrap is blocked until the initial navigation is complete. This value is required for server-side rendering to work. ā€˜legacy_enabledā€™- (Default, for compatibility.) The initial navigation starts after the root component has been created. The bootstrap is not blocked until the initial navigation is complete.

That said, if initialNavigation: 'enabled' was added automatically when upgrading as you say, this is a bug in the migration since the initialNavigation options are not equivalent.

Hi, any update of when is this going to be merged? Thanks!

Hi guys, hereā€™s the workarounds Iā€™m applying on my project (v11). It works with SRR as well. Hope it helps someone

  • There will be a ReplaySubject userInfoSubject to inform the authentication status
  • APP_INITIALIZER will call AuthenticationService to check for authentication status and emit the retrieved authentication status by userInfoSuject.next(userInfo). Note that whenever the userInfo changes (by sign in, sign upā€¦) we need to emit the authentication status too
  • CanActivate will subscribe to this replay subject
// app.module.ts
function initializeApp(
    authenticationService: AuthenticationService,
): () => Promise<any> | undefined {
    return () => {
        if (typeof window !== 'undefined') {
            return authenticationService.getUserInfo().toPromise();
        }
        return;
    };
}
// authentication.service.ts
export class AuthenticationService {
    userInfo: UserLoginInfo | null | undefined;
    userInfoSubject = new ReplaySubject<UserLoginInfo | null>(1);

    constructor(
        @Inject(PLATFORM_ID) private platformId: Record<string, unknown>,
        @Optional()
        @Inject('authenticatedUserData')
        public authenticatedUserDataFromSsr: UserLoginInfo | null | undefined,
        private httpClient: HttpClient,
        private transferStateService: TransferStateService,
    ) {
        if (isPlatformServer(this.platformId)) {
            this.transferStateService.set<UserLoginInfo | null | undefined>(
                'authenticatedUserData',
                authenticatedUserDataFromSsr
            );
        }
        this.userInfo = this.transferStateService.get('authenticatedUserData');
        if (this.isFetched(this.userInfo)) {
            this.userInfoSubject.next(this.userInfo);
        }
    }

    isFetched(data: any): boolean {
        return typeof data !== 'undefined';
    }

    getUserInfo(): Observable<UserLoginInfo | null | undefined> {
        if (this.isFetched(this.userInfo)) {
            return of(this.userInfo);
        }
        return this.httpClient.get<{ user: UserLoginInfo | null }>('/userData').pipe(
            map((response) => response.user),
            catchError((err) => {
                this.userInfoSubject.next(null);
                return of(null);
            }),
            tap((userInfo) => {
                this.userInfo = userInfo;
                this.userInfoSubject.next(userInfo);
            })
        );
    }
...
}
// authentication.guard.ts
export class AuthenticationGuard implements CanActivate {
    constructor(private authenticationService: AuthenticationService, private router: Router) {}

    canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
    ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
        return this.authenticationService.userInfoSubject.pipe(
            map((userInfo) => {
                if (userInfo) {
                    return true;
                }
                return this.router.parseUrl('/');
            })
        );
    }
}

@nils-thomann, I believe that really depends on your project. For example, in my project I have dynamic routes, which I only know by doing an API Call. So now I have a few assumptions in my routes (so, now itā€™s not 100% dynamic, but like 80%) and I have a Resolver that makes the same call that I used to do in APP_INITIALIZER.

Yep, same issue here. I ā€œresolvedā€ by adding an ā€œif isPlatformBrowser(this.platformId)ā€ in each guardā€¦

Hi, Iā€™m facing the same issue here too