Skip to content

Implement retry logic for network timeouts when attempting to load the discovery document #1492

Description

@aschambers

I think it would be great if the library handled connection timeouts better to avoid situations like

  • Mobile Safari's aggressive connection management
  • Network timeouts during page lifecycle
  • Background/foreground transitions
  • Any transient network failures

Here's an example of what I did to fix this within my own application, so thinking something along these lines would be great to add to this library to retry in case this kind of issue happens (I'm using a library called exponential-backoff):

Inside of my app.config.ts file:

provideAppInitializer(initializeAuth),
provideAppInitializer(initializeAuthVisibilityListener),

async function initializeAuth() {
  const authService = inject(AuthService);
  const loggingService = inject(LoggingService);

  try {
    await authService.loadDiscoveryDocumentWithRetry();
  } catch (error: unknown) {
    loggingService.logError(error);
    throw error;
  }
}

function initializeAuthVisibilityListener() {
  const authService = inject(AuthService);
  const loggingService = inject(LoggingService);

  if (typeof document === 'undefined') {
    return;
  }

  let lastVisibleTime = Date.now();
  const handleVisibilityChange = async () => {
    if (document.hidden) {
      lastVisibleTime = Date.now();
      return;
    }

    const timeInBackground = Date.now() - lastVisibleTime;
    if (timeInBackground > 30000) {
      try {
        await authService.loadDiscoveryDocumentWithRetry(true);
      } catch (error: unknown) {
        loggingService.logError(error);
      }
    }

    lastVisibleTime = Date.now();
  };

  document.addEventListener('visibilitychange', handleVisibilityChange);
}

Inside of my Auth Service:

async loadDiscoveryDocument(shouldReloadDiscoveryDocument: boolean = false) {
    if (
      !this.oauthService?.discoveryDocumentLoaded ||
      shouldReloadDiscoveryDocument
    ) {
      await this.oauthService.configure(authCodeFlowConfig);
      await this.oauthService.loadDiscoveryDocument();
    }
  }

  async loadDiscoveryDocumentWithRetry(
    shouldReloadDiscoveryDocument: boolean = false
  ): Promise<void> {
    const maxRetries = 5;

    return await backOff(
      () => this.loadDiscoveryDocument(shouldReloadDiscoveryDocument),
      {
        numOfAttempts: maxRetries,
        startingDelay: 1000,
        timeMultiple: 2,
        maxDelay: 30000,
        jitter: 'full',
        retry: (error: unknown) =>
          this.shouldRetryLoadingDiscoveryDocument(error)
      }
    );
  }

  async loadDiscoveryDocumentAndTryLoginWithRetry() {
    const maxRetries = 5;

    return await backOff(
      () => this.oauthService.loadDiscoveryDocumentAndTryLogin(),
      {
        numOfAttempts: maxRetries,
        startingDelay: 1000,
        timeMultiple: 2,
        maxDelay: 30000,
        jitter: 'full',
        retry: (error: unknown) =>
          this.shouldRetryLoadingDiscoveryDocument(error)
      }
    );
  }

  shouldRetryLoadingDiscoveryDocument(error: unknown) {
    const isHttpError = error instanceof HttpErrorResponse;
    const isNetworkError = isHttpError && error.status === 0;
    const isGenericError = error instanceof Error;
    const hasHttpFailureMessage =
      isGenericError && error.message.includes('Http failure response');
    const shouldRetry = isNetworkError || hasHttpFailureMessage;
    const params =
      error && typeof error === 'object' && 'params' in error
        ? (error as any).params
        : null;
    const error_description =
      params && typeof params === 'object' && 'error_description' in params
        ? String(params.error_description)
        : '';
    const lowerErrorDescription = error_description?.toLowerCase() || '';
    if (lowerErrorDescription.includes('user cancelled authentication')) {
      return false;
    }

    return shouldRetry;
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions