How to handle unauthorized requests(status with 401 or 403) with new httpClient in angular 4.3
S

3

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

Stonemason answered 2/9, 2017 at 19:6 Comment(6)
HttpClient supports interceptors. You should use that to handle errors transparently, instead of forcing all your code to use ApiService instead of HttpClient directly.Stardom
@JBNizet I also have an auth interceptor. I have updated the post. Is that the place to handle such requests? Any sample code??Stonemason
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.Spaetzle
@Spaetzle 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 postStonemason
added an example in an answerSpaetzle
Hey, you got any solution..? Because I got this same error but it couldn't solve it.Platitude
S
81

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

Spaetzle answered 2/9, 2017 at 19:32 Comment(10)
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.Stonemason
you need a factory function to provide an httpinterceptor and injec the router... see this github gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4cSpaetzle
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 injectorsSpaetzle
Yeah true, I just got the same error. Looks like I need a factory function like in the gist link you provided.Stonemason
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.Spaetzle
yeah sure. I was expecting that this would be handled in new httpClient. But couldn't find any thing related.Stonemason
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.Spaetzle
Let us continue this discussion in chat.Stonemason
I got the impression from the angular documentation on HttpClient (angular.io/guide/http#why-write-a-service) we were supposed to ue a service to access our api. Did I got that wrong ?Arequipa
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.Spaetzle
S
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 ...
]
Stonemason answered 3/9, 2017 at 0:51 Comment(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.Spaetzle
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.Stonemason
C
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.

Chetnik answered 8/6, 2018 at 13:36 Comment(1)
how do you inject storageService?Capricecapricious

© 2022 - 2024 — McMap. All rights reserved.