3

I just discovered a bug in our software - pretty sure it used to work, but now it's broken.

Plunkr reproducing the issue can be found here: https://embed.plnkr.co/BNRcQFWGkkEitxjwE7pi/

In our app.module we have set up providers for APP_INITIALIZER, APP_BASE_HREF and ErrorHandler in three different factories

AppInitializerFactory

Calls an async load method for our config, which is stored in a json file. In this sample loads baseHref and submitToSentry

ErrorHandler

A custom errorhandler that submits to Sentry, if config tells it to. In constructor config is not loaded yet - and that is acceptable. Something about having to setup ErrorHandler or APP_INITIALIZER first. They had to pick one. Instead we look at config once an error occurs - and if we should submitToSentry - we do.

BaseHrefFactory

Was intended to set the APP_BASE_HREF injection token to whatever baseHref in Config was. As the Plunkr shows, baseHref printed ends up being undefined - not myBaseHref as expected from config.json.

If... I comment out the AppRoutingModule in app.module.ts:52 - then it works.

Question is... (As I need the AppRoutingModule)

I guess I'm once again (as with the errorhandler) is in a race condition between setting up routing and initializing the app - And somewhere internally in @angular/router/routerModule they inject APP_BASE_HREF.

Are there any way to achieve that APP_INITIALIZER holds back routing module until done?

Am I just holding it wrong? Is there something in the ordering or similar that can fix this.

Hoping for someone to be able to explain me - have used quite a few hours debugging it myself - and is currently lost.

Glad though that the Plunkr could reproduce it.

ankhansen
  • 853
  • 9
  • 20

1 Answers1

3

[Too late now I'm sure, but I'll have a go at answering this. Note that although the plunkr still shows the code, it doesn't actually run - like most plunkr examples on SO these days]

Your provider setup has: provide: APP_INITIALIZER, useFactory: AppInitializerFactory, deps: ConfigServiceBase and although it's not obvious from the function declaration (since you're not being explicit about the type of the return value), AppInitializerFactory returns a function that returns a promise. This is important, since the Angular initialization process will wait for that promise to be resolved before continuing to load the rest of the app (loading the initial component, etc).

You also have provide: APP_BASE_HREF, useFactory: BaseHrefFactory, deps: ConfigServiceBase. But, since this is not associated with APP_INITIALIZER, Angular makes no particular guarantee as to when the BaseHrefFactory will be executed (though it's unlikely to be executed until something asks for the APP_BASE_HREF). The fact that it sometimes gets executed after AppInitializerFactory may be pure luck.

To be clear, providers using the APP_INITIALIZER token are guaranteed to be instantiated before the first component is created - but this is unrelated to the order in which services are instantiated. The dependency between services is expressed via the dependency-injection tree.

Note that the Angular router makes use of APP_INITIALIZER itself - some of the initialization of the router happens there. So, it's possible that by adding routing into the mix you change the (already random) order in which things happen.

Another thing to bear in mind is that, although BaseHrefFactory has a dependency on ConfigServiceBase, it does not have a dependency on "an instance of ConfigService whose load() method has been called". The injector will quite happily pass it a reference to an instance of ConfigService whose load() method has not been called.

To solve all this, I would inject a service rather than try to inject a value, as follows.

1) Create a service to hold the value:

@Injectable()
export class AppBaseHrefService {
  public baseHref: string;
}

2) Modify the AppInitializerFactory function to take an AppBaseHrefService parameter, and add an extra step to set the value based on the config:

export function AppInitializerFactory(config: ConfigServiceBase, appBaseHrefService: AppBaseHrefService) {
return () => config.load()
    .then(() => {
          appBaseHrefService.baseHref = config.baseHref
    });

}

3) Give your APP_INITIALIZER provider a dependency on AppBaseHrefService:

{ provide: APP_INITIALIZER, useFactory: AppInitializerFactory, deps: [ConfigServiceBase, AppBaseHrefService], multi: true }

Of course, you may decide that it's easier to simply inject the existing ConfigService / ConfigServiceBase rather than injecting yet another service. I'd be inclined to agree, unless your actual ConfigService has lots of configuration information and you only want to expose some of it.


UPDATE July 2019, since no-one seems to be able to get this to work...

Firstly, please bear in mind that I'm not claiming to be able to set the value provided by APP_BASE_HREF. Instead, I'm suggesting that you could inject something else that will provide the value you need.

I've created a StackBlitz that shows this in action. Here I have the following moving parts:

  1. I have an InitializerProvider provider defined in the AppModule and associated with the APP_INITIALIZER DI token. This uses a factory function to do asyncronous initialization. Because that function returns a Promise, no components will be created etc. until that promise has been resolved (i.e. initialization is complete).

  2. The factory function above uses the ConfigService to asynchronously fetch "some data". It then uses that data to initialize the BaseUrlService. More synchronous or asynchronous actions could be added here as necessary. (Async/await is great for this stuff).

  3. Components (e.g. AppComponent in this example) can inject BaseUrlService and use that to access the base URL that's derived from the data fetched during initialization. Since the initialization happens before any components are created, there should be no circumstances in which those components see an un-initialized service.

HTH.

Gary McGill
  • 26,400
  • 25
  • 118
  • 202
  • Hi Gary, I'm pretty hung up with a delivery needed to ship soon, so will not get to look at this for the next two weeks - but will try to revert once i've delivered our current sprint. But thanks for taking your time to answer - i am still interested in a solution so I don't need to know the basehref at compiletime - but can configure it at each customer site, based in their wanted internal web structures. Currently my site needs to run in root at all customers, and thus seperate domain/port needs to be setup - and not just a subsite. – ankhansen Mar 05 '19 at 08:35
  • 2
    Hi Gary Got the time to look at it now. I can't get your idea to work either - and I think the problem is the same, no matter what I depend on. The APP_BASE_HREF is a special variable, and no matter how I write AppIntializerFactory, I can't get it to wait on the async call to the api. The () => load.then() still returns immediately - and call the BaseHrefFactory before the load has finished. – ankhansen May 15 '19 at 10:56
  • 1
    Can confirm that this solution unfortunately does **not** work for me either; `APP_BASE_HREF` always comes first for me, and only sees the unresolved value :-( – qqilihq Jul 22 '19 at 14:16
  • @qqilihq I've added an example - and also clarified that I'm *not* saying you can change the value provided by `APP_BASE_HREF`, but rather offering an alternative approach. – Gary McGill Jul 22 '19 at 22:22
  • @ankhansen: Please see update, in case it helps or at least explains what I meant. – Gary McGill Jul 22 '19 at 22:23
  • @GaryMcGill Thank you for the clarification -- I admittedly didn’t get that point, that your solution was **not** for changing `APP_BASE_HREF`. I'll post my solution later here or in any of the other questions. – qqilihq Jul 23 '19 at 06:28
  • @GaryMcGill - thx for the update. What I needed was the actual value of APP_BASE_HREF to be set. As I recall it, since routing uses that to create links when using e.g. [routerLink] in anchors. And with APP_BASE_HREF being set dynamically, this didn't work. Currently all our sites run in roots, and that is now just a requirement for customers wanting to run it on-premise. If we ever get a hard requirement for not running in root of a domain, I will consider alternatives - but will leave this thread as it is for now. Thanks for your involvement both of you. – ankhansen Aug 05 '19 at 06:34