简体   繁体   中英

APP_INITIALIZER not awaiting (Angular 13)

I am in the process of updating from okta/okta-angular 3.x to 5.x. It seems to have introduced an odd bug.

When the app first starts up, we have been using APP_INITIALIZER to execute appInitializerFactory(configService: ConfigService) , which makes an http call to load configuration data.

The call looks like this:

  public async loadConfig(): Promise<any> {
    return this.httpClient.get('assets/config.json').pipe(settings => settings)
      .toPromise()
      .then(settings => {
        this.config = settings as IAppConfig;
      })
      .catch(exception => {
        console.log("Exception encountered while retreiving configuration");
      });
  }

Before updating to okta 5.x, the APP_INITIALIZER has been awaiting the promise. Now, it appears that other providers are being resolved before the promise in APP_INITILIZER has finished.

The resulting issue happens downstream at the oktaInitializerFactory , where it runs the following code:

public oktaConfig() {
    return Object.assign({
      onAuthRequired: (oktaAuth: OktaAuth, injector: Injector) => {
        const router = injector.get(Router);
        router.navigate(['/login']);
      }
    }, this.config.oktaConfig);
  }

On the last line, this.config.oktaConfig is returning as undefined because the APP_INITIALIZER has not finished awaiting.

Heres the complete app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { Routes, RouterModule, Router } from '@angular/router';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { MaterialModule } from './modules/material/material.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TablesComponent } from './components/tables/tables.component';
import { PageNotFoundComponent } from './components/page-not-found/page-not-found.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
import { LoginComponent } from './components/login/login.component';
import { FormsModule } from '@angular/forms';
import { RunLogComponent } from './components/run-log/run-log.component';
import { RunDataComponent } from './components/run-data/run-data.component';
import { ActionComponent } from './components/action/action.component';
import { ErrorsComponent } from './components/errors/errors.component';
import { OktaAuth } from '@okta/okta-auth-js';
import { OKTA_CONFIG, OktaAuthGuard, OktaAuthModule, OktaCallbackComponent } from '@okta/okta-angular';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { TabulatorUserTableComponent } from './components/tabulator-user-table/tabulator-user-table.component';
import { TabulatorTableComponent } from './components/tabulator-table/tabulator-table.component';
import { DataLakeComponent } from './components/data-lake/data-lake.component';
import { IntegrationHistoryComponent } from './components/integration-history/integration-history.component';
import { IntegrationStatusComponent } from './components/integration-status/integration-status.component';
import { MatSpinnerButtonComponent } from './components/mat-spinner-button/mat-spinner-button.component';
import { ConfigService } from './services/config.service';
import { DataStudioComponent } from './components/data-studio/data-studio.component';
import { IntegrationDashboardComponent } from './components/integration-dashboard/integration-dashboard.component';
import { IntegrationSelectorToolbarComponent } from './integration-selector-toolbar/integration-selector-toolbar.component';
import { PrimaryButtonsComponent } from './components/primary-buttons/primary-buttons.component';
import { UserCardComponent } from './components/user-card/user-card.component';
import { HttpOktaInterceptorService } from './services/http-okta-interceptor.service';
import { DebugInfoComponent } from './components/debug-info/debug-info.component';
import { ModalModule } from './modal';
import { DragNDrop } from './components/dropbox/drag-n-drop';
import { ProgressComponent } from './components/dropbox/progress/progress.component';


const appRoutes: Routes = [
  {
    path: '',
    component: DashboardComponent,
    canActivate: [OktaAuthGuard]
  },
  {
    path: 'login/callback',
    component: OktaCallbackComponent
  },
  {
    path: 'login',
    component: LoginComponent
  }
];

@NgModule({
  declarations: [
    AppComponent,
    TablesComponent,
    PageNotFoundComponent,
    DashboardComponent,
    ToolbarComponent,
    LoginComponent,
    RunLogComponent,
    RunDataComponent,
    ActionComponent,
    ErrorsComponent,
    TabulatorUserTableComponent,
    TabulatorTableComponent,
    DataLakeComponent,
    IntegrationHistoryComponent,
    IntegrationStatusComponent,
    MatSpinnerButtonComponent,
    DataStudioComponent,
    IntegrationDashboardComponent,
    IntegrationSelectorToolbarComponent,
    PrimaryButtonsComponent,
    UserCardComponent,
    DebugInfoComponent,
    ProgressComponent,
    DragNDrop
  ],
  imports: [
    BrowserModule,
    OktaAuthModule,
    RouterModule.forRoot(appRoutes, { relativeLinkResolution: 'legacy' }),
    BrowserAnimationsModule,
    MaterialModule,
    FormsModule,
    HttpClientModule,
    ModalModule
  ],
  providers: [
    { 
      provide: APP_INITIALIZER, 
      useFactory: appInitializerFactory, 
      deps:[ConfigService], 
      multi: true
    },
    { 
      provide: OKTA_CONFIG, 
      useFactory: oktaInitializerFactory, 
      deps:[ConfigService],
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpOktaInterceptorService,
      multi: true,
      deps: [OktaAuth]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

export function appInitializerFactory(configService: ConfigService) {
  return () => configService.loadConfig();
}

export function oktaInitializerFactory(configService: ConfigService) {
  return configService.oktaConfig();
}

Is there a particular reason why my APP_INITIALIZER isn't finishing before other code executes? When I downgrade back to okta 3.x, this issue goes away.

As it turns out, upgrading from Okta 3 to 4+ or 5+ does introduce a change that makes loading auth server configuration in APP_INITIALIZER a non-viable option.

In a nutshell, okta-angular will attempt to connect to the auth server before the APP_INITIALIZER finishes. The workaround for this is to load the configuration data into the injector in main.ts , which fires off before the APP_INITIALIZER . This technically goes against the documentation in angular.io , but Okta support has verified this behavior [ Source ]

Another user does a similar implementation for Auth0, which encountered the same problem, linked here: stackoverflow.com/a/66957293/3202440

I have no idea how it was supposed to work in previous versions. you have appInitializerFactory that depends on ConfigService . ConfigService depends on HttpClient . HttpClient => HTTP_INTERCEPTORS ie on HttpOktaInterceptorService . HttpOktaInterceptorService => OktaAuth which most likely relies on OKTA_CONFIG .

It means that to construct a HttpClient (which is implicitly requried for initializer) you need OKTA_CONFIG already constructed. and it sounds very logical that this config initializer is being called earlier than APP_INITIALIZER s.

Most likely this dependency was introduced during refactoring, not just the update.

In your place I would try to eliminate dependency on a HttpClient in ConfigService and just make this request with a native api

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM