37

I have an auth-interceptor.service.ts to handle the requests

import {Injectable} from '@angular/core';
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {Cookie} from './cookie.service';
import {Router} from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private router: Router) {}
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Clone the request to add the new header.
        const authReq = req.clone({headers: req.headers.set(Cookie.tokenKey, Cookie.getToken())});
        // Pass on the cloned request instead of the original request.
        return next.handle(authReq).catch(this.handleError);
    }

    private handleError(err: HttpErrorResponse): Observable<any> {
        console.log(err);
        if (err.status === 401 || err.status === 403) {
            Cookie.deleteUser();
            this.router.navigateByUrl(`/login`);
            return Observable.of(err.message);
        }
        // handle your auth error or rethrow
        return Observable.throw(err);
    }
}

But I get the following error. Nothing really happens like it doesn't delete the cookie or it doesn't navigate to login page Any help or suggestions would be appreciated.

enter image description here

Community
  • 1
  • 1
Sai Ram Gupta
  • 1,143
  • 2
  • 9
  • 26
  • HttpClient supports interceptors. You should use that to handle errors transparently, instead of forcing all your code to use ApiService instead of HttpClient directly. – JB Nizet Sep 02 '17 at 19:15
  • @JBNizet I also have an auth interceptor. I have updated the post. Is that the place to handle such requests? Any sample code?? – Sai Ram Gupta Sep 02 '17 at 19:26
  • if you go with your method you're going to be needing to wrap the entire http service. I'd recommend either using an interceptor or extending the http service and overriding the request method only. – bryan60 Sep 02 '17 at 19:27
  • @bryan60 I also have an interceptor to add header to each request. I am not sure how to handle the response from any request in the interceptor. I have updated the post – Sai Ram Gupta Sep 02 '17 at 19:29
  • added an example in an answer – bryan60 Sep 02 '17 at 19:36
  • Hey, you got any solution..? Because I got this same error but it couldn't solve it. – ankita patel Jul 09 '18 at 06:47

3 Answers3

79

You should use your interceptor and just handle it like this:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private router: Router) { }

    private handleAuthError(err: HttpErrorResponse): Observable<any> {
        //handle your auth error or rethrow
        if (err.status === 401 || err.status === 403) {
            //navigate /delete cookies or whatever
            this.router.navigateByUrl(`/login`);
            // if you've caught / handled the error, you don't want to rethrow it unless you also want downstream consumers to have to handle it as well.
            return of(err.message); // or EMPTY may be appropriate here
        }
        return throwError(err);
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Clone the request to add the new header.
        const authReq = req.clone({headers: req.headers.set(Cookie.tokenKey, Cookie.getToken())});
        // catch the error, make specific functions for catching specific errors and you can chain through them with more catch operators
        return next.handle(authReq).pipe(catchError(x=> this.handleAuthError(x))); //here use an arrow function, otherwise you may get "Cannot read property 'navigate' of undefined" on angular 4.4.2/net core 2/webpack 2.70
    }
}

no need for the http service wrapper.

to use the router you'll need a factory provider like:

 providers: [
     {
         provide: HTTP_INTERCEPTORS,
         useFactory: function(router: Router) {
           return new AuthInterceptor(router);
         },
         multi: true,
         deps: [Router]
      },
      .... other providers ...
  ]

where ever you're providing the interceptor (probably app.module). don't use an arrow function. they aren't supported in factory functions when you try to build for prod.

Working plunk: https://plnkr.co/edit/UxOEqhEHX1tCDVPDy488?p=preview

bryan60
  • 28,215
  • 4
  • 48
  • 65
  • I really appreciate your response and added to my code. I got some weird error. I am not sure why is `this.router` undefined. I have updated the post with response. Thank your for the help. – Sai Ram Gupta Sep 02 '17 at 19:49
  • you need a factory function to provide an httpinterceptor and injec the router... see this github https://gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c – bryan60 Sep 02 '17 at 19:50
  • I believe it is to use the router in an interceptor, http interceptors are registered before the router so it can't be injected without it due to angular's hierarchichal injectors – bryan60 Sep 02 '17 at 20:07
  • Yeah true, I just got the same error. Looks like I need a factory function like in the gist link you provided. – Sai Ram Gupta Sep 02 '17 at 20:09
  • I added an example of a factory provider, the answer in that github uses an arrow function in the factory function, which you should NOT do. your builds will fail. – bryan60 Sep 02 '17 at 20:10
  • yeah sure. I was expecting that this would be handled in new httpClient. But couldn't find any thing related. – Sai Ram Gupta Sep 02 '17 at 20:19
  • nope, not really a thing that can be handled due to how t he injectors work. I added a working plunkr of catching errors in an interceptor. – bryan60 Sep 02 '17 at 20:53
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/153537/discussion-between-sai-ram-gupta-and-bryan60). – Sai Ram Gupta Sep 03 '17 at 00:57
  • I got the impression from the angular documentation on HttpClient (https://angular.io/guide/http#why-write-a-service) we were supposed to ue a service to access our api. Did I got that wrong ? – Agustin Cautin Mar 14 '18 at 07:59
  • You didn’t get that wrong, you just misinterpreted this question and answer. This is for a specific case of intercepting authentication failures To redirect the user. So you’d use an interceptor rather tha wrappping angulars native http client. If you want to interact with your own api you should definitely create a service and inject the http client into it. But if you want to intercept authentication errors with your api and handle them, you should use an interceptor. – bryan60 Mar 14 '18 at 20:13
5

From the @bryan60 suggestion I made few changes to his solution

In app.module.ts:

providers: [
     {
        provide: HTTP_INTERCEPTORS,
        useFactory: function(injector: Injector) {
            return new AuthInterceptor(injector);
        },
        multi: true,
        deps: [Injector]
    },
      .... other providers ...
]

and in auth-interceptor.service.ts:

import {Injectable, Injector} from '@angular/core';
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {Cookie} from './cookie.service';
import {Router} from '@angular/router';
import {UserService} from './user.service';
import {ToasterService} from '../toaster/toaster.service';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}

    private handleError(err: HttpErrorResponse): Observable<any> {
        let errorMsg;
        if (err.error instanceof Error) {
            // A client-side or network error occurred. Handle it accordingly.
            errorMsg = `An error occurred: ${err.error.message}`;
        } else {
            // The backend returned an unsuccessful response code.
            // The response body may contain clues as to what went wrong,
            errorMsg = `Backend returned code ${err.status}, body was: ${err.error}`;
        }
        if (err.status === 404 || err.status === 403) {
            this.injector.get(UserService).purgeAuth();
            this.injector.get(ToasterService).showError(`Unauthorized`, errorMsg);
            this.injector.get(Router).navigateByUrl(`/login`);
        }
        console.error(errorMsg);
        return Observable.throw(errorMsg);
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Clone the request to add the new header.
        const authReq = req.clone({headers: req.headers.set(Cookie.tokenKey, Cookie.getToken())});
        // Pass on the cloned request instead of the original request.
        return next.handle(authReq).catch(err => this.handleError(err));
    }
}

If you are using AOT in building try:

export function authInterceptorFactory(injector: Injector) {
    return new AuthInterceptor(injector);
}

providers: [
         {
            provide: HTTP_INTERCEPTORS,
            useFactory: authInterceptorFactory,
            multi: true,
            deps: [Injector]
        },
          .... other providers ...
]
Sai Ram Gupta
  • 1,143
  • 2
  • 9
  • 26
  • 2
    Why use the injector instead of injecting explicitly? Your service now has 3 somewhat hidden dependencies. It's not a big deal if you only do it in one place, but if you make it a habit, your project could become hard to modify and maintain. – bryan60 Sep 03 '17 at 07:49
  • Yeah thanks for the info. This is the only place I am doing like this and this is imported directly to app module. I made sure no other module imports this interceptor. – Sai Ram Gupta Sep 03 '17 at 17:39
2

the above @bryan60 answer is works fine , if any one facing issue like me with catch the error in below line

return next.handle(authReq).catch(x=> this.handleAuthError(x));

using do() handle the error(if you face issue with catch())

import in file:

import 'rxjs/add/operator/do';

handle error:

return next.handle(authReq)
 .do(
    success => {/*todo*/},
    err => {this.handleAuthError(authReq)}
    );
}

handleAuthError(err: any) {
    if(err.status === 401 || err.status === 403) {
    this.storageService.clear();
    window.location.href = '/home';
    }
}

I hope this is help someone.

yala ramesh
  • 3,362
  • 1
  • 22
  • 35