11

I want to make a call to a server that can return an authorization fail (401) with Angular2's HTTP class.

The flow of the request should look like that:

  • The user makes a request to the server with myService.getSomething().subscribe()
  • If the server returns a 401: open a modal window asking the user for his credentials.
  • The user successfully log back into the application
  • The modal closes and executes a callback
  • The callback should retry the initial request (myService.getSomething().subscribe())

Here is what I have for the moment:

export class MyService {
    // ...
    public getSomething(): Observable<Response> {
        return this.http.get(url, options).catch((res: any, ob: any) => this.errorHandler(res, ob));
    }

    public errorHandler(res: Response, ob: Observable<any>): Observable<Response> {
        if (res.status === 401) {
            this.modalService.open(new ModalConfig({
                content: LoginModalComponent,
                close: () => { ob.retry(1); console.log("weow") } // <=close is the callback that should initiate the retry action on the initial request.
            }));
        }
        else {
            return Observable.throw(res.json());
        }
    }
}

doSomething() is used like that: doSomething().map((r) => r.json()).subscribe((r) => ....)

Update 1

I modified my code to look like @Thierry Templier's solution.

private errorHandler(res: Response, ob: Observable<any>): Observable<Response> {
    if (res.status === 401) {
        let closedSubject = new Subject();
        this.modalService.open(new ModalConfig({
            content: LoginModalComponent,
            close: () => { closedSubject.next(res);} // I also tried .complete(), .next(null), .next(true), .next(false)
        }));
        return ob.retryWhen(() => closedSubject);
    }
    else {
        return Observable.throw(res.json());
    }
}

Sadly it still doesn't work. The retryWhen is executed right away and doesn't wait for closedSubject.next() to be called. Therefore it starts an infinite loop, spamming the original Observable (the getSomething() function).

Update 2

I created a plunker to demonstrate the infinite loop:

https://plnkr.co/edit/8SzmZlRHvi00OIdA7Bga

Warning: running the plunker will spam your console with the string 'test'

Update 3

Following Thierry's correct answer, I tried to find a way to not use the source field since it is protected. After asking on rxjs's issue tracker to make the field public, a contributor replied with a better solution.

public get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    return super.get(url, options).retryWhen((errors: any) => this.errorHandler(errors));
}
private errorHandler(errors): any {
    return errors.switchMap((err) => {
        if (err.status === 401) {
            let closedSubject = new Subject();
            this.modalService.open(new ModalConfig({
                content: LoginModalComponent,
                close: () => { closedSubject.next(err); }
            }));
            return <any>closedSubject;
        }
        else {
            return Observable.throw(err.json());
        }
    });
}

I avoid using .catch so I don't have to use the source field.

Jean-Philippe Leclerc
  • 6,713
  • 5
  • 43
  • 66

3 Answers3

9

I think that you need to return something an observable even in the case of the 401 error:

public errorHandler(res: Response, ob: Observable<any>): Observable<Response> {
    if (res.status === 401) {
        let closedSubject = new Subject();
        this.modalService.open(new ModalConfig({
            content: LoginModalComponent,
            close: () => {
              closedSubject.next();
        }));
        return ob.retryWhen(() => closedSubject);
    }
    else {
        return Observable.throw(res.json());
    }
}

See this article for more details: https://jaxenter.com/reactive-programming-http-and-angular-2-124560.html.

Edit

The problem is that the second parameter of the catch callback isn't the source observable. This source observable corresponds to the value of its source property:

return ob.source.retryWhen((errors) => closedSubject);

See the working plunkr: https://plnkr.co/edit/eb2UdF9PSMhf4Dau2hqe?p=preview.

Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • I feel like this is close to the answer but it still doesn't work. When the sever throws a 401, the errorHandler enters an infinite loop. It retries the request without waiting for the modal to close. – Jean-Philippe Leclerc Apr 15 '16 at 20:23
  • 1
    I don't know what you did in your login modal but I guess that you need to set something in your request before executing it again. For this reason, I think that you shouldn't use retry but execute again the request based on its url and the updated option (for example, the `Authorization` header)... – Thierry Templier Apr 15 '16 at 21:00
  • The problem is that the retryWhen is resolved right away. It is not waiting for closeSubject.next() to be called. – Jean-Philippe Leclerc Apr 15 '16 at 21:06
  • Strange. It shouldn't. That said there was a typo in my snippet. I updated my answer accordingly... – Thierry Templier Apr 15 '16 at 21:26
  • See my "edit" section and the plunkr: https://plnkr.co/edit/eb2UdF9PSMhf4Dau2hqe?p=preview. – Thierry Templier Apr 18 '16 at 20:14
  • @ThierryTemplier after retrying, if the request fails again, why does the whole chain die? I mean, shouldn't it retry again and again and again? – Milad Dec 05 '17 at 23:16
2

I guess retryWhen operator should help.

kemsky
  • 14,727
  • 3
  • 32
  • 51
0

I believe the solution has changed a little, since in the news versions of Angular we must use the pipe() method. So I decided to use a custom operator solution. One good thing is the handleError() method could be exported as a global function and then be used in more than one service.

See this solution for more details: https://blog.angularindepth.com/retry-failed-http-requests-in-angular-f5959d486294

export class MyService {
// ...
public getSomething(): Observable<Response> {
    return this.http.get(url, options).pipe(this.handleError('Maybe your a custom message here'));
}

private handleError(errorMessage: string) {
    return (source: Observable<any>) => source.pipe(
        retryWhen(errors => errors.pipe(
            mergeMap((errorResponse: HttpErrorResponse) => {
                console.error(errorMessage);
                if (errorResponse.status === 401) {
                    const closedSubject = new Subject();
                    this.modalService.open(new ModalConfig({
                        content: LoginModalComponent,
                        close: () => {
                            closedSubject.next();
                        }
                    }));
                    return closedSubject;
                }
                return throwError(errorResponse);
            })
        ))
    );
}

}

Felipe Desiderati
  • 2,414
  • 3
  • 24
  • 42