Hi I am trying to figure out how implement the new angular interceptors and handle 401 unauthorized
errors by refreshing the token and retrying the request. This is the guide I have been following: https://ryanchenkie.com/angular-authentication-using-the-http-client-and-http-interceptors
I am successfully caching the failed requests and can refresh the token but I cannot figure out how to resend the requests that previously failed. I also want to get this to work with the resolvers I am currently using.
token.interceptor.ts
return next.handle( request ).do(( event: HttpEvent<any> ) => { if ( event instanceof HttpResponse ) { // do stuff with response if you want } }, ( err: any ) => { if ( err instanceof HttpErrorResponse ) { if ( err.status === 401 ) { console.log( err ); this.auth.collectFailedRequest( request ); this.auth.refreshToken().subscribe( resp => { if ( !resp ) { console.log( "Invalid" ); } else { this.auth.retryFailedRequests(); } } ); } } } );
authentication.service.ts
cachedRequests: Array<HttpRequest<any>> = []; public collectFailedRequest ( request ): void { this.cachedRequests.push( request ); } public retryFailedRequests (): void { // retry the requests. this method can // be called after the token is refreshed this.cachedRequests.forEach( request => { request = request.clone( { setHeaders: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${ this.getToken() }` } } ); //??What to do here } ); }
The above retryFailedRequests() file is what I can't figure out. How do I resend the requests and make them available to the route through the resolver after retrying?
This is all the relevant code if that helps: https://gist.github.com/joshharms/00d8159900897dc5bed45757e30405f9
As you probably already know this interceptor can be used to modify any incoming or outgoing HTTP requests. How do we get the benefits of an Angular interceptor and use it to retry any failed HTTP request? The answer is simple. To add the retry capability we'll use the retry function that triggers a retry when the Observable fails.
In your interceptor on response refresh the token and return the error. When your service gets back the error but now retry operator is being used so it will retry the request and this time with the refreshed token (Interceptor uses refreshed token to add in the header.) Show activity on this post.
Other than that retry operator may help with your logic of refreshing token on 401 response. Use the RxJS retry operator in your service where you are making a request. It accepts a retryCount argument. If not provided, it will retry the sequence indefinitely. In your interceptor on response refresh the token and return the error.
It accepts a retryCount argument. If not provided, it will retry the sequence indefinitely. In your interceptor on response refresh the token and return the error.
My final solution. Works with parallel requests.
UPDATE: The code updated with Angular 9 / RxJS 6, error handling and fix looping when refreshToken fails
import { HttpRequest, HttpHandler, HttpInterceptor, HTTP_INTERCEPTORS } from "@angular/common/http"; import { Injector } from "@angular/core"; import { Router } from "@angular/router"; import { Subject, Observable, throwError } from "rxjs"; import { catchError, switchMap, tap} from "rxjs/operators"; import { AuthService } from "./auth.service"; export class AuthInterceptor implements HttpInterceptor { authService; refreshTokenInProgress = false; tokenRefreshedSource = new Subject(); tokenRefreshed$ = this.tokenRefreshedSource.asObservable(); constructor(private injector: Injector, private router: Router) {} addAuthHeader(request) { const authHeader = this.authService.getAuthorizationHeader(); if (authHeader) { return request.clone({ setHeaders: { "Authorization": authHeader } }); } return request; } refreshToken(): Observable<any> { if (this.refreshTokenInProgress) { return new Observable(observer => { this.tokenRefreshed$.subscribe(() => { observer.next(); observer.complete(); }); }); } else { this.refreshTokenInProgress = true; return this.authService.refreshToken().pipe( tap(() => { this.refreshTokenInProgress = false; this.tokenRefreshedSource.next(); }), catchError(() => { this.refreshTokenInProgress = false; this.logout(); })); } } logout() { this.authService.logout(); this.router.navigate(["login"]); } handleResponseError(error, request?, next?) { // Business error if (error.status === 400) { // Show message } // Invalid token error else if (error.status === 401) { return this.refreshToken().pipe( switchMap(() => { request = this.addAuthHeader(request); return next.handle(request); }), catchError(e => { if (e.status !== 401) { return this.handleResponseError(e); } else { this.logout(); } })); } // Access denied error else if (error.status === 403) { // Show message // Logout this.logout(); } // Server error else if (error.status === 500) { // Show message } // Maintenance error else if (error.status === 503) { // Show message // Redirect to the maintenance page } return throwError(error); } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> { this.authService = this.injector.get(AuthService); // Handle request request = this.addAuthHeader(request); // Handle response return next.handle(request).pipe(catchError(error => { return this.handleResponseError(error, request, next); })); } } export const AuthInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true };
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With