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?