2

I'm using Angular 2 and RxJS to retrieve data from my API layer. I have a base service that I've written to deal with HTTP requests. The following code does this within my service:

protected apiGet(uri: string): Observable<HttpMappedResponse<T>> {
    return this.mapAndHandle<T>(this.http.get(uri));
}

private mapAndHandle<T>(observable: Observable<Response>): Observable<HttpMappedResponse<T>> {
    const o = observable
        .map(res => {
            const data = res.json() || {};
            const mappedResponse = new HttpMappedResponse<T>(res, data);
            // Retrieve page information from HTTP headers
            mappedResponse.pageInfo = new PageInfo(res.headers);
            return mappedResponse;
        })
        .share();

    o.subscribe(null, e => this.errorService.handleHttpResponse(e));
    return o;
}

This works well and gives me all the data I need when I call apiGet(). What I'm now trying to do is the following:

  • Send a HTTP request
  • If a 401 error with a particular string is returned:
    • Send another request to a different URL
    • Update another service
    • Retry the original request
    • (then probably add logic to handle possible infinite retries)
  • Else
    • Rethrow the original http error
  • Capture error and handle it

I've tried using .catch(), .retryWhen(), .map() and .flatMapTo() with no success - closest I've got is the following:

enum HttpVerb {
    Put,
    Post,
    Get,
    Delete
}

private mapAndHandle<T>(uri: string, type: HttpVerb): Observable<HttpMappedResponse<T>> {
    const restObservable = this.getRestObservable(uri, type);

    const o = restObservable
        .map(res => this.mapToHttpMappedResponse<T>(res))
        .catch((error, sourceObservable) => {
            const data = error.json() || {};

            if (error.status === 401 && data.Message === 'Token expired') {
                console.log('Retrying');
                // Refresh the expired auth token
                const refreshToken = this.authService.userData.RefreshToken;
                console.log(refreshToken);

                this.authService
                    .refresh(new RefreshRequestModel(refreshToken))
                    .subscribe(x => null);

                // Retry original
                return this
                    .getRestObservable(uri, type)
                    .map(x => this.mapToHttpMappedResponse(x));
            }
            throw error;
        })
        .share();

    o.subscribe(null, e => {
        this.errorService.handleHttpResponse(e);
    });
    return o;
}

private mapToHttpMappedResponse<T>(res: Response): HttpMappedResponse<T> {
    console.log('Attempt ' + Date.now());
    const data = res.json() || {};
    const mappedResponse = new HttpMappedResponse<T>(res, data);
    // Retrieve page information from HTTP headers
    mappedResponse.pageInfo = new PageInfo(res.headers);
    return mappedResponse;
}

But this doesn't give me the right results.

Is there an operator that I can use to do this? retryWhen() seems the most logical, but I just keep getting myself into a mess with RxJs!

Spikeh
  • 3,540
  • 4
  • 24
  • 49
  • Let me know if my answer leads you in the right direction. It is all pseudocode, but it should give you a general idea of what you can do (you already know the steps you need to take). Just leverage `flatMap` when retrying your request and you can keep it in the same observable stream. – Lansana Camara Aug 02 '17 at 16:34

1 Answers1

4

One of the problems you are having is you are not chaining your calls. Your refresh call should be chained to your retry call, and they should leverage flatMap or something similar to keep it in the same observable stream.


Take a look at my answer to this question.

This is how you can achieve it:

You need to add the catch operator from RxJS. This is where an error will be, and you can handle it accordingly.

In order to get your refresh logic to go through the interceptor, you need to return the invocation, and the function should also return an Observable. For example modifying the original logic above:

this.http.request(url, options)
        .map((res: Response) => res.json())
        .catch((error: any) => {
            const err = error.json();

            // Refresh JWT
            if (err.status === 401) {
                // refreshToken makes another HTTP call and returns an Observable.
                return this.refreshToken(...);
            }

            return Observable.throw(err);
        });

If you want to be able to retry the original request, what you can do is pass the original request data so that on successfully refreshing the token you can make the call again. For example, this is what your refreshToken function could look like:

refreshToken(url: string, options: RequestOptionsArgs, body: any, tokenData: any): Observable<any>
    return this.post(`${this.url}/token/refresh`, tokenData)
        .flatMap((res: any) => {
            // May want to use response data to set new tokens in your local storage or whatever here...

            // This is where I retry the original request
            return this.request(url, options, body);
        });
}
Lansana Camara
  • 9,497
  • 11
  • 47
  • 87
  • 1
    Thank you! I've seen examples of flatMap() but couldn't really figure out what it was doing before now. I also spent a few hours tracking down a stupid refactoring bug that I introduced into my code that was replacing the body in my POST and PUT commands with the headers. That certainly did not help my understanding here! – Spikeh Aug 02 '17 at 17:44
  • Glad I could help :) I was stuck on this for a while as well but finally found out I could leverage `flatMap` to solve it. You can probably make this much cleaner, but you get the idea now. – Lansana Camara Aug 02 '17 at 18:05
  • 1
    My full implementation is quite complex - the nature of trying to make things as generic as possible :) – Spikeh Aug 02 '17 at 18:13
  • this.request(url, options, body); this gave me the final clue, I had common errorhandler for Get/Post/Put . – Code Name Jack Apr 05 '20 at 15:01