Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 4 - intercept http request and resend after login

I have an HttpInterceptor which listens to specific JWT token events (token_expired, token_not_provided and token_invalid) that can happen at different times of the workflow.

These events can happen when a user navigates to a different route OR when an AJAX request is being sent while in the same route (like retrieving data, saving a form, etc).

When the interceptor detects any of those specific events, it prompts the user to enter login credentials again (using a modal) and queues the request for later processing (after user logged in again). This is important since the data which was submitted can not be lost (e.g. when updating an order or a customer).

A Simplified version of my interceptor code is:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}
    router: Router;
    auth: AuthService;
    api: APIService;
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        this.router = this.injector.get(Router);
        this.auth = this.injector.get(AuthService);
        let token = this.auth.getToken();
        let headers = {
            'Content-Type':'application/json',
        };
        if (token) {
            (<any>headers).Authorization =  `Bearer ${token}`;
        }

        request = request.clone({
            setHeaders: headers
        });
        return next.handle(request).do((event: HttpEvent<any>) => {

        }, (err: any) => {
            if (err instanceof HttpErrorResponse) {
                let msg = typeof(err.error) === 'string' ? JSON.parse(err.error) : err.error;
                if (msg && msg.error && ['token_not_provided', 'token_expired','token_invalid'].indexOf(msg.error) > -1) {
                        this.auth.queueFailedRequest(request);
                        //set the intended route to current route so that after login the user will be shown the same screen
                        this.auth.setIntendedRoute(this.router.url);
                        //show the login popup
                        this.auth.promptLogin();
                    }
                }
            }
        });
    }
}

The relevant part of AuthService is:

 queue: Array<HttpRequest<any>> = [];
 queueFailedRequest(request): void {
    this.queue.push(request);
 }

 retryFailedRequests(): void {
        this.queue.forEach(request => {
            this.retryRequest(request);
        });
        this.queue = [];
 }
 retryRequest(request): void {
        if (request.method === 'GET') {
             this.apiService.get(request.urlWithParams);
        }
        else if (request.method === 'POST') {
             this.apiService.post(request.urlWithParams, request.body || {});
        }
    }

And, of course, after a successful login I call the retryFailedRequests().

So far so good and indeed all HTTP requests are being queued and sent if a login is successful.

And now to the problem - if the code is stuctured as in this example (taken from an EditOrder component):

updateOrder() {
   this.api.updateOrder(this.data).subscribe(res => {
     if (res.status === 'success') {
        alert('should be triggered even after login prompt');
     }
  });
}

Then, if the user needs to re-login in the process, the alert will never be triggered once the retryFailedRequests() method finished processing the queue.

So the question is what is the best way to make sure that the original promise is queued along with the HTTP request and resolved when the queue finished processing?

like image 287
dev7 Avatar asked May 27 '26 14:05

dev7


1 Answers

So I was having the same issue and we ultimately solved it by wrapping the Observable that handle() returns in our own Observable, and returning that from intercept().

In the below example, this.requestQueue is just an array of objects where we can store the Subscriber from the new Observable, the original HttpRequest, and the original HttpHandler. We do this to queue requests if authentication isn't yet complete.

So here's the intercept method:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Return a new Observable
    return new Observable<HttpEvent<any>>((observer) => {
        if (this.authInProgress) {
            this.requestQueue.push({eventObserver: observer, request: req, handler: next});
        } else {
            this.processRequest(observer, req, next);
        }
    });

}

So whoever made the initial HTTP request gets to subscribe to an observable that won't complete until we say so, this is crucial. The key is that we keep a reference to the Subscriber object (observer) so we can use it later to get responses back to where they belong. But we haven't actually handled the request in this method.

Here is processRequest():

private processRequest(eventObserver, request, handler) {
    // Handle the request
    // - pass the response along on success
    // - handle 401 errors and pass along all others
    handler.handle(request).subscribe(
        (event: HttpEvent<any>) => { eventObserver.next(event); },
        (err: any) => {
            if (err instanceof HttpErrorResponse && err.status === 401) {
                if (!this.authInProgress) {
                    // If this is the first 401 then we kick off some
                    // auth processes and mark authInProgress as true.
                    this.authInProgress = true;
                }

                // Save this request for later
                this.requestQueue.push({eventObserver, request, handler});
            } else {
                eventObserver.error(err);
            }
        },
        () => { eventObserver.complete(); }
    );
}

So in processRequest(), we actually send the request by calling handler.handle(request). We subscribe right away and if the request succeeds, we send the response event on its way by calling eventObserver.next(event). That will send the response back to whoever subscribed to the Observable that we returned in intercept().

If we get an error and it's a 401, we just save the request for later just like we did in intercept().

Later on, when we're ready to process all those queued requests, we pop() them out of the requestQueue and pass them to processRequest(). This time, assuming your auth worked, they won't error and the success responses will get back to the upstream subscriber.

like image 75
binary lobster Avatar answered May 30 '26 02:05

binary lobster