0
votes

I'm new at using Angular and Rxjs.

Basically an app consumes an Api and needs to provide a valid authentication token. This token is short lived, and if it has expired it can be refreshed.

I wrote a specific http service which add the correct headers to every request and in case of authentication failure tries to refresh the token and retry.

Below is how I implemented it:

export class HttpAService extends Http {

  constructor(
    backEnd: XHRBackend,
    options: RequestOptions,
    private authenticationService: AuthenticationService
  ) {
    super(backEnd, options);
    // get an authentication token from an authentication service
    let token = authenticationService.token;
    options.headers.set('Authorization', `Bearer ${token}`);
  }

  private request1(
    url: string | Request,
    options?: RequestOptionsArgs
  ): Observable<Response> {
    // get a token from an authentication service
    let token = this.authenticationService.token;
    // add it to the request headers
    if (typeof url === 'string') {
      if (!options) {
        options = { headers: new Headers };
      }
      options.headers.set('Authorization', `Bearer ${token}`);
    } else {
      url.headers.set('Authorization', `Bearer ${token}`);
    }
    // make the request
    return super.request(url, options);
  }

  public request(
    url: string | Request,
    options?: RequestOptionsArgs
  ): Observable<Response> {
    // init retry flag
    let retried: boolean = false;
    // make the request and retry one time in case of failure
    const obs = Observable.defer(() => this.request1(url, options));
    return obs.retryWhen(attempt => {
      // request failed
      return attempt.flatMap(res => {
        // if it's the first round and an authentication error try to refresh token
        if ((!retried) && (500 === res.status) && ('Unauthenticated.' == res.json().message)) {
          retried = true;
          // refresh attempts to create a new token that can be retrieved using authenticationService.getToken.
          return this.authenticationService.refresh();
        }
        return Observable.throw(res);
      })
    })
  }
}

The problem I ran into was to have the new token used by the retry request (in other words to have the request1 function rerun during the retry). It seem that defer(...) did the trick.

I'd like to know if this implementation is correct and if there is a more elegant way to implement this behavior.

Thank you for your responses.

1

1 Answers

0
votes

For those that sill arrive at this question I think the recommended operator is retryWhen. Here is an example.

get(url:string): Observable<Object> {
  return this.httpClient.get(url, {headers: this.authHeaders}).pipe(
    retryWhen(errors => errors.pipe(
      switchMap((e:HttpErrorResponse) => {
        if (e.status === 403) return this.refreshAuthToken();
        console.warn("HTTP GET error", url, e); //like 500 internal server error
        throw e;
      })
    ))
  )
}

But there is a flaw. While refreshAuthToken works perfectly the original HTTP observable is still stuck with the token that is not authenticated. Not sure how to get around this.

So for now I use a solution that works, but it recursively calls my get method instead of using an operator.

get(url:string): Observable<Object> {
  return this.httpClient.get(url, {headers: this.authHeaders}).pipe(
    catchError((e:HttpErrorResponse) => {
      if (e.status === 403) {
        return this.refreshAuthToken().pipe(
          switchMap(success => success? this.get(url) : of(null)) //recursive call not cool :(
        )
      }
      console.warn("HTTP GET error", url, e); //like 500 internal server error
      return of(null);
    })
  )
}

Hope someone provides a better answer.