Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 7: Await function in interceptor

I have built an error interceptor in my first Angular application which is all new for me. The interceptor tries to refresh a Firebase authorization token when a 401 response code occures. Therefore I have written the following code:

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService, private alertService: AlertService) { }

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    return throwError(err.error);
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    return throwError(err.error);
                }
                const reference = this;
                this.authService.getToken(currentUser, true).then(t => {
                    // How do I await and return this properly?
                    return reference.updateTokenAndRetry(request, next, currentUser, t);
                }); // Get token and refresh
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            return throwError(err.error);
        })
    );
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}

The token gets refreshed fine. However the network call is not being executed after the refresh, which reference.updateTokenAndRetry(request, next, currentUser, t); should do.

I assume the reason for this, is that this.authService.getToken(currentUser, true) returns a Promise (this is the Firebase plugin and can't be changed). I want to return return reference.updateTokenAndRetry(request, next, currentUser, t); but this is not possible since it's in an async function block.

How can I await or return the next network call? I can't make the intercept function async. I am pretty stuck at this point.

like image 322
gi097 Avatar asked May 29 '19 11:05

gi097


2 Answers

Instead of trying to return an async promise you should convert your promise to an observable using the RxJS 'from' operator as described in this post: Convert promise to observable.

This will result in a correct return type of Observable> for your interceptor.

Your code would look like something like the following (assuming you only send one request at a time):

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    return throwError(err.error);
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    return throwError(err.error);
                }
                // Return a newly created function here
                return this.refreshToken(currentUser, request, next);
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            return throwError(err.error);
        })
    );
}

refreshToken(currentUser: any, request: any, next: any) {
    // By making use of the from operator of RxJS convert the promise to an observable
    return from(this.authService.getToken(currentUser, true)).pipe(
        switchMap(t => this.updateTokenAndRetry(request, next, currentUser, t))
    )
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}

Hope this helps!

like image 184
Arwin Avatar answered Nov 05 '22 17:11

Arwin


Arwin solution works well, but only in an environment where one request is send at the time.

In order to get this to work save the refreshToken method into an Observable with the pipe share. This will allow multiple subscribers but a single result.

Wrap the next.handle(request) method in another Subject<any> and return the subject. If the request fires an error call that isn't a 401 error call subject.error.

After refreshing the token call this.updateTokenAndRetry(request, next, currentUser, token).subscribe(result => subject.next(result); to make sure that the request is returned to the initial subscriber.

Below code is pseudo code and should work in your case.

refreshTokenObservable: Observable<any>;

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    let subject = new Subject<any>();

    next.handle(request).pipe(
        catchError(err => {
            if (err.status === 401) {
                let user = localStorage.getItem('currentUser');
                if (!user) {
                    this.logout(false);
                    subject.error(err.error);
                    return;
                }
                let currentUser = JSON.parse(user);
                if (!currentUser || !currentUser.stsTokenManager || !currentUser.stsTokenManager.accessToken) {
                    this.logout(false);
                    subject.error(err.error);
                    return;
                }
                // Return a newly created function here
                this.refreshToken(currentUser).subscribe(token => {

                    this.updateTokenAndRetry(request, next, currentUser, token).subscribe(result => subject.next(result);
                    this.refreshTokenObservable = null; // clear observable for next failed login attempt
                });
            }
            this.alertService.showAlert({
                text: 'Fout tijdens het verzenden van het verzoek',
            });
            subject.error(err.error);
        })
    ).subscribe(result => subject.next(result));

    return subject.asObservable();
}

refreshToken(currentUser: any) {

    if(this.refreshTokenObservable == null)
    {
        // By making use of the from operator of RxJS convert the promise to an observable
        this.refreshTokenObservable = from(this.authService.getToken(currentUser, true)).pipe(share());
    }

    return this.refreshTokenObservable;
}

updateTokenAndRetry(request: HttpRequest<any>, next: HttpHandler, currentUser: any, token: string): Observable<HttpEvent<any>> {
    // Update local stored user
    currentUser.stsTokenManager.accessToken = token;
    localStorage.setItem('currentUser', JSON.stringify(currentUser));

    // Add the new token to the request
    request = request.clone({
        setHeaders: {
            Authorization: token,
        },
    });

    return next.handle(request);
}
like image 1
Mark Verkiel Avatar answered Nov 05 '22 17:11

Mark Verkiel