getlarge/ticketing

refactor: replace @ory/client built-in axios

getlarge opened this issue · 0 comments

To improve error responses handling for calls to Ory APIs, it would be useful to provide a custom axios instance.
This instance would have custom interceptors to wrap errors in standard class, and would allow to add retry strategies in case of rate-limiting.

The BaseAPI class from @ory/client, which all API extends, allow to pass a custom axios instance in the constructor.
The OryPermissionsService and OryAuthenticationService could make use of this improvement.

In OryPermissionsModule and OryAuthenticationModule, import HttpModule :

// ...
declare module 'axios' {
  interface AxiosRequestConfig {
    retries?: number;
    retryCondition?: (error: AxiosError) => boolean;
    retryDelay?: (error: AxiosError, retryCount: number) => number;
  }
}

const HttpModuleWithRetry = HttpModule.register({
  timeout: 5000,
  responseType: 'json',
  validateStatus(status: number) {
    return status >= 200 && status < 300;
  },
  retries: 3,
  retryCondition(error) {
    const statusToRetry = [429];
    return error.response?.status
      ? statusToRetry.includes(error.response?.status)
      : false;
  },
  retryDelay(error, retryCount) {
    if (error.response?.status === 429) {
      const headers = error.response.headers;
      const remaining = headers['x-ratelimit-remaining'];
      const resetTimestamp = headers['x-ratelimit-reset'];
      if (Number(remaining) === 0) {
        return Number(resetTimestamp) * 1000 - Date.now();
      }
    }
    return retryCount * 250;
  },
});

// ...

In OryPermissionsService, inject HttpService, create axios interceptors and pass custom axios instance:

  constructor(
    @Inject(OryPermissionsModuleOptions) options: OryPermissionsModuleOptions,
    @Inject(HttpService) private readonly httpService: HttpService,
  ) {
    this.httpService.axiosRef.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (isAxiosError(error)) {
          if ('response' in error) {
            const { config, response } = error;
            const shouldRetry = config?.retryCondition(error) ?? true;
            if (config?.retries && shouldRetry) {
              const retryDelay =
                config?.retryDelay(error, config.retries) ?? 250;
              config.retries -= 1;
              await delay(retryDelay);
              return this.httpService.axiosRef(config);
            }
            const oryError = new OryError(error);
            return Promise.reject(oryError);
          }
          const oryError = new OryError(error);
          return Promise.reject(oryError);
        }
        const oryError = new OryError(error);
        return Promise.reject(oryError);
      },
    );
    const { ketoAccessToken, ketoAdminApiPath, ketoPublicApiPath } = options;
    this.relationShipApi = new RelationshipApi(
      new Configuration({
        basePath: ketoAdminApiPath,
        accessToken: ketoAccessToken,
      }),
      '',
      this.httpService.axiosRef,
    );
    this.permissionApi = new PermissionApi(
      new Configuration({
        basePath: ketoPublicApiPath,
        accessToken: ketoAccessToken,
      }),
      '',
      this.httpService.axiosRef,
    );
  }