93

In my Angular 2 project I make API calls from services that return an Observable. The calling code then subscribes to this observable. For example:

getCampaigns(): Observable<Campaign[]> {
    return this.http.get('/campaigns').map(res => res.json());
}

Let's say the server returns a 401. How can I catch this error globally and redirect to a login page/component?

Thanks.


Here's what I have so far:

// boot.ts

import {Http, XHRBackend, RequestOptions} from 'angular2/http';
import {CustomHttp} from './customhttp';

bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS,
    new Provider(Http, {
        useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => new CustomHttp(backend, defaultOptions),
        deps: [XHRBackend, RequestOptions]
    })
]);

// customhttp.ts

import {Http, ConnectionBackend, Request, RequestOptions, RequestOptionsArgs, Response} from 'angular2/http';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class CustomHttp extends Http {
    constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
        super(backend, defaultOptions);
    }

    request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {

        console.log('request...');

        return super.request(url, options);        
    }

    get(url: string, options?: RequestOptionsArgs): Observable<Response> {

        console.log('get...');

        return super.get(url, options);
    }
}

The error message I'm getting is "backend.createConnection is not a function"

Nicolas Henneaux
  • 11,507
  • 11
  • 57
  • 82
pbz
  • 8,865
  • 14
  • 56
  • 70

8 Answers8

87

Angular 4.3+

With the introduction of HttpClient came the ability to easily intercept all requests / responses. The general usage of HttpInterceptors is well documented, see the basic usage and how to provide the interceptor. Below is an example of an HttpInterceptor that can handle 401 errors.

Updated for RxJS 6+

import { Observable, throwError } from 'rxjs';
import { HttpErrorResponse, HttpEvent, HttpHandler,HttpInterceptor, HttpRequest } from '@angular/common/http';

import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status == 401) {
          // Handle 401 error
        } else {
          return throwError(err);
        }
      })
    );
  }

}

RxJS <6

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).do(event => {}, err => {
            if (err instanceof HttpErrorResponse && err.status == 401) {
                // handle 401 errors
            }
        });
    }
}
The Gilbert Arenas Dagger
  • 12,071
  • 13
  • 66
  • 80
  • 1
    Is this still working for you? Yesterday it was working for me but after installing other modules, I am getting this error: next.handle(…).do is not a function – Multitut Aug 23 '17 at 18:39
  • I think this one should be used as extension of classes like http is almost always a smell – kboom Aug 28 '17 at 06:51
  • 1
    Don't forget to add it to your providers list with the HTTP_INTERCEPTORS. You can find an example in the [docs](https://angular.io/guide/http#providing-your-interceptor) – Bruno Peres Oct 20 '17 at 13:45
  • 2
    Great but using `Router` in here doesn't seem to work. For instance, I want to route my users to the log in page when they get a 401-403, but `this.router.navigate(['/login']` is not working for me. It does nothing – CodyBugstein Jan 18 '18 at 23:58
  • If you are getting ".do is not a function", add `import 'rxjs/add/operator/do';` after you import rxjs. – amoss Aug 08 '18 at 18:25
  • how about tap instead – Matt Broekhuis Jan 30 '19 at 23:50
80

Description

The best solution I have found is to override the XHRBackend such that the HTTP response status 401 and 403 leads to a particular action.

If you handle your authentication outside your Angular application you could force a refresh of the current page such that your external mechanism is triggered. I detail this solution in the implementation below.

You could also forward to a component inside your application such that your Angular application is not reloaded.

Implementation

Angular > 2.3.0

Thanks to @mrgoos, here is a simplified solution for angular 2.3.0+ due to a bug fix in angular 2.3.0 (see issue https://github.com/angular/angular/issues/11606) extending directly the Http module.

import { Injectable } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';


@Injectable()
export class AuthenticatedHttpService extends Http {

  constructor(backend: XHRBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options).catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
  }
}

The module file now only contains the following provider.

providers: [
    { provide: Http, useClass: AuthenticatedHttpService }
]

Another solution using Router and an external authentication service is detailed in the following gist by @mrgoos.

Angular pre-2.3.0

The following implementation works for Angular 2.2.x FINAL and RxJS 5.0.0-beta.12.

It redirects to the current page (plus a parameter to get a unique URL and avoid caching) if an HTTP code 401 or 403 is returned.

import { Request, XHRBackend, BrowserXhr, ResponseOptions, XSRFStrategy, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

export class AuthenticationConnectionBackend extends XHRBackend {

    constructor(_browserXhr: BrowserXhr, _baseResponseOptions: ResponseOptions, _xsrfStrategy: XSRFStrategy) {
        super(_browserXhr, _baseResponseOptions, _xsrfStrategy);
    }

    createConnection(request: Request) {
        let xhrConnection = super.createConnection(request);
        xhrConnection.response = xhrConnection.response.catch((error: Response) => {
            if ((error.status === 401 || error.status === 403) && (window.location.href.match(/\?/g) || []).length < 2) {
                console.log('The authentication session expires or the user is not authorised. Force refresh of the current page.');
                window.location.href = window.location.href + '?' + new Date().getMilliseconds();
            }
            return Observable.throw(error);
        });
        return xhrConnection;
    }

}

with the following module file.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule, XHRBackend } from '@angular/http';
import { AppComponent } from './app.component';
import { AuthenticationConnectionBackend } from './authenticated-connection.backend';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
    ],
    entryComponents: [AppComponent],
    imports: [
        BrowserModule,
        CommonModule,
        HttpModule,
    ],
    providers: [
        { provide: XHRBackend, useClass: AuthenticationConnectionBackend },
    ],
})
export class AppModule {
}
Nicolas Henneaux
  • 11,507
  • 11
  • 57
  • 82
  • Does this solution still work for RC4? I'm getting an error about `.catch()` not being a method on undefined. Would appreciate your thoughts/input. – hartpdx Jul 25 '16 at 21:48
  • I have just made an update which is supported by RC.4 – Nicolas Henneaux Jul 26 '16 at 14:34
  • 2
    Thanks! I figured out my problem...I was missing this line, which is why `catch()` wasn't found. (smh) `import "rxjs/add/operator/catch";` – hartpdx Jul 26 '16 at 15:35
  • 1
    Is it possible to use the Router module to do the navigation? – Yuanfei Zhu Sep 11 '16 at 04:17
  • I think it is possible using gards https://angular.io/docs/ts/latest/guide/router.html#!#guards but I have not try it. – Nicolas Henneaux Sep 11 '16 at 07:33
  • 1
    Great solution for bundling with Auth Guard! 1. Auth Guard checks authorized user (e.g. by looking into LocalStorage). 2. On 401/403 response you clean authorized user for the Guard (e.g. by removing coresponding parameters in LocalStorage). 3. As at this early stage you can't access the Router for forwarding to the login page, refreshing the same page will trigger the Guard checks, which will forward you to the login screen (and optionally preserve your initial URL, so you'll be forwarded to the requested page after successful authentication). – Alex Klaus Oct 14 '16 at 05:00
  • I was checking the use of AuthenticationConnectionBackend but I had a problem, The Response doesn't have the response headers, have you ever checked that? – Carlos E. Feria Vila Nov 11 '16 at 22:00
  • 1
    Hey @NicolasHenneaux - why do you think that it's better then to override `http`? The only benefit I see is that you can simply put it as a provider: `{ provide: XHRBackend, useClass: AuthenticationConnectionBackend }` while when overriding Http you need to write more awkward code like `useFactory` and limit yourself by calling 'new' and sending specific arguments. WDYT? A reference to the 2nd method: http://www.adonespitogo.com/articles/angular-2-extending-http-provider/ – mrgoos Nov 13 '16 at 17:00
  • @mrgoos at the time I have implemented this class, the Http service did not have any common method to override such that I got this behavior (it seems it is not the case anymore but it must be tested). I have thus overridden the XHRBackend which is one lower level than Http. – Nicolas Henneaux Nov 13 '16 at 18:58
  • @NicolasHenneaux - cool. I like yours better, so far, I'm just considering one of these options. Thanks. – mrgoos Nov 13 '16 at 20:02
  • 1
    I did find one issue when using this method though. You cannot inject your own services to the constructor, There's an open issue about that which will, hopefully, be resolved soon: https://github.com/angular/angular/pull/8991 – mrgoos Nov 14 '16 at 10:42
  • Anyone know of any workarounds to inject your own service to the constructor? Or any alternative solutions that work? – Bryan Dec 01 '16 at 02:06
  • I think it is fixed but I am not sure on which version it is available (see https://github.com/angular/angular/issues/11606#issuecomment-262315256) – Nicolas Henneaux Dec 01 '16 at 06:49
  • 1
    I upgraded to 2.3 and am now getting the following error in the console: Can't resolve all parameters for AuthenticationConnectionBackend : (?, ?, ?) – Bryan Dec 12 '16 at 00:10
  • 3
    @Brett - I've created a gist for it which should help you: https://gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c – mrgoos Dec 12 '16 at 11:17
  • How do you inject your own service with this new solution for 2.3? When I tried I got the following build error: "Supplied parameters do not match any signature of call target" – Bryan Dec 13 '16 at 22:13
  • @Brett I have just tested with 2.4 version and it works with custom service injection. – Nicolas Henneaux Dec 21 '16 at 12:57
24

As frontend APIs expire faster than milk, with Angular 6+ and RxJS 5.5+, you need to use pipe:

import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 401) {
          this.router.navigate(['login'], { queryParams: { returnUrl: req.url } });
        }
        return throwError(err);
      })
    );
  }
}

Update for Angular 7+ and rxjs 6+

import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';
import { catchError } from 'rxjs/internal/operators';
import { Router } from '@angular/router';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((err, caught: Observable<HttpEvent<any>>) => {
          if (err instanceof HttpErrorResponse && err.status == 401) {
            this.router.navigate(['login'], { queryParams: { returnUrl: request.url } });
            return of(err as any);
          }
          throw err;
        })
      );
  }
}

Saeb Amini
  • 23,054
  • 9
  • 78
  • 76
12

The Observable you get from each request method is of type Observable<Response>. The Response object, has an status property which will hold the 401 IF the server returned that code. So you might want to retrieve that before mapping it or converting it.

If you want to avoid doing this functionality on each call you might have to extend Angular 2's Http class and inject your own implementation of it that calls the parent (super) for the regular Http functionality and then handle the 401 error before returning the object.

See:

https://angular.io/docs/ts/latest/api/http/index/Response-class.html

Matt
  • 74,352
  • 26
  • 153
  • 180
Langley
  • 5,326
  • 2
  • 26
  • 42
  • So if I extend Http then I should be able to redirect to a "login" route from within the Http? – pbz Jan 21 '16 at 21:13
  • That's the theory. You'll have to inject the router into your Http implementation to do it. – Langley Jan 21 '16 at 21:16
  • Thanks for your help. I've updated the question with a sample code. I'm probably doing something wrong (being new to Angular). Any idea what it could be? Thanks. – pbz Jan 21 '16 at 23:12
  • You are using the default Http providers, you have to create your own provider that resolves to an instance of your class instead of the default one. See: https://angular.io/docs/ts/latest/api/core/Provider-class.html – Langley Jan 21 '16 at 23:18
  • It I add "new Provider(Http, { useClass: CustomHttp })" to boostrap I get "No provider for CustomHttp". Should I use a factory? – pbz Jan 21 '16 at 23:31
  • Yes, and the factory should be a method that creates a new object with your class – Langley Jan 22 '16 at 00:02
  • I've updated boot.ts (original post) to use a provider (factory), but I'm getting the same error message (back to the original one). Thoughts? Thanks. – pbz Jan 22 '16 at 00:36
  • I dont think you need to override request, and dont use connection backend, use an implementation of it, like XHR, look at the example I posted – Langley Jan 22 '16 at 00:39
  • I didn't realize ConnectionBackend was an abstract class and that it needed concrete class instead. Thanks for your help!! Tomorrow I'll tackle the routing part of it. – pbz Jan 22 '16 at 00:50
  • @Gustavo Not yet; I believe that part of the code (routing) is still baking. For now, I decided to just log the error and not show the UI. Once Angular2 matures a bit more I will come back to this (hopefully I'll remember to update this questions as well). – pbz Feb 17 '16 at 00:56
  • Hi guys, I am a little confused. To me it seems that Angular2 Http handles 401/403 as error. If not (Response.status > 200 and Response.status < 300), it will give an error. So, I cannot even check whether my login service was down or I simply entered the wrong credentials. Anybody? – abedurftig Mar 04 '16 at 15:50
  • @dasnervtdoch even if it errors out you still have access to the Response.status to check what was the exact error. – Langley Mar 04 '16 at 16:31
  • 1
    @Langley, thanks. You are right: subscribe((result) => {}, (error) => {console.log(error.status);}. The error parameter is still of type Response. – abedurftig Mar 04 '16 at 17:08
  • one of my servers with cakephp give no 401 status error, but i see it on the browser, another backend with nodejs is ok ,giving 401 code status code, must to extends http just to get the 401 error ??? – stackdave Jun 27 '17 at 11:36
  • @Langley if you have a server that does not send status code 401 ( i have one in cakephp ), with extend Angular 2's Http , i get the same 0 status value. With another server (nodejs) i get correct 401 status code, so with extend http or not i get the same ... it's really hard get from angular just the code like the browser? – stackdave Jun 27 '17 at 11:46
  • @stackdave extending http has nothing to do with the error code retrieved or not, that was just a suggestion if you wanted to handle it in the same way through the app, when you make any request log the response in the error callback and you'll probably see the error code there, you should see it directly in the response object although some implementations add it in the response.data object. – Langley Jun 27 '17 at 15:15
  • thanks, can you copy the code how to add the response.data object? i really need read the header – stackdave Jun 27 '17 at 15:18
  • https://angular.io/guide/http when you subscribe to the response observable you can pass two callbacks, the first one is in case of success and the second one in case of error, in both you'll get the response object and you can print it in the console to see its contents and find out exactly which property you are after – Langley Jun 27 '17 at 19:21
9

To avoid the cyclic referencing issue that is caused by having services like "Router" being injected into an Http derived class, one must use the post-constructor Injector method. The following code is a working implementation of an Http service that redirects to Login route each time a REST API returns "Token_Expired". Note that it can be used as a substitution to the regular Http and as such, doesn't require to change anything in your application's already existing components or services.

app.module.ts

  providers: [  
    {provide: Http, useClass: ExtendedHttpService },
    AuthService,
    PartService,
    AuthGuard
  ],

extended-http.service.ts

import { Injectable, Injector } from '@angular/core';
import { Request, XHRBackend, RequestOptions, Response, Http, RequestOptionsArgs, Headers } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/throw';

@Injectable()
export class ExtendedHttpService extends Http {
    private router; 
    private authService;

  constructor(  backend: XHRBackend, defaultOptions: RequestOptions, private injector: Injector) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
 
    if (typeof url === 'string') {
      if (!options) {
        options = { headers: new Headers() };
      }
      this.setHeaders(options);
    } else {
      this.setHeaders(url);
    }
    console.log("url: " + JSON.stringify(url) +", Options:" + options);

    return super.request(url, options).catch(this.catchErrors());
  }

  private catchErrors() {

    return (res: Response) => {
        if (this.router == null) {
            this.router = this.injector.get(Router);
        }
        if (res.status === 401 || res.status === 403) {
            //handle authorization errors
            //in this example I am navigating to login.
            console.log("Error_Token_Expired: redirecting to login.");
            this.router.navigate(['signin']);
        }
        return Observable.throw(res);
    };
  }

  private setHeaders(objectToSetHeadersTo: Request | RequestOptionsArgs) {
      
      if (this.authService == null) {
            this.authService = this.injector.get(AuthService);
      }
    //add whatever header that you need to every request
    //in this example I could set the header token by using authService that I've created
     //objectToSetHeadersTo.headers.set('token', this.authService.getToken());
  }
}
Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
Tuthmosis
  • 1,131
  • 3
  • 15
  • 27
9

Angular 4.3+

To complete The Gilbert Arenas Dagger answer:

If what you need is intercept any error, apply a treatment to it and forward it down the chain (and not just add a side effect with .do), you can use HttpClient and its interceptors to do something of the kind:

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // install an error handler
        return next.handle(req).catch((err: HttpErrorResponse) => {
            console.log(err);
            if (err.error instanceof Error) {
                // A client-side or network error occurred. Handle it accordingly.
                console.log('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,
                console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
            }

            return Observable.throw(new Error('Your custom error'));
        });
    }
}
Starscream
  • 1,128
  • 1
  • 9
  • 22
8

From Angular >= 2.3.0 you can override the HTTP module and inject your services. Before version 2.3.0, you couldn't use your injected services due to a core bug.

I've created a gist to show how it's done.

mrgoos
  • 1,294
  • 14
  • 26
  • Thanks for putting that together. I was getting a build error that said "Cannot find name 'Http'" In app.module.ts, so I imported and am now getting the following error: "Cannot instantiate cyclic dependency! Http: in NgModule AppModule" – Bryan Dec 13 '16 at 21:16
  • Hey @Brett- can you share your `app.module` code? Thanks. – mrgoos Dec 13 '16 at 22:42
  • It seems OK. Can you add to the gist the extended HTTP? In addition, do you import `HTTP` anywhere else? – mrgoos Dec 27 '16 at 12:24
  • Sorry for the delay. I am on Angular 2.4 now and getting same error. I import Http in several files. Here is my updated gist: https://gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3 – Bryan Jan 14 '17 at 22:31
  • Same issue here... Looks like this gist isn't working so maybe we should mark it as such ? – Tuthmosis Apr 17 '17 at 18:13
  • Hey @Tuthmosis , why isn't it working? It's still working in my code. Can you please share what issues do you have? – mrgoos Apr 17 '17 at 18:17
  • Because of the Cyclic reference issue... I'll post my solution below. – Tuthmosis Apr 19 '17 at 17:38
  • So it's an edge case, doesn't mean that the solution is not working – mrgoos Apr 20 '17 at 13:55
  • It's not working for me either. Getting the same "cannot instantiate cyclic dependency" error. – Aamir Khan Apr 25 '17 at 06:44
2

Angular >4.3: ErrorHandler for the base service

protected handleError(err: HttpErrorResponse | any) {
    console.log('Error global service');
    console.log(err);
    let errorMessage: string = '';

    if (err.hasOwnProperty('status')) { // if error has status
        if (environment.httpErrors.hasOwnProperty(err.status)) {
            // predefined errors
            errorMessage = environment.httpErrors[err.status].msg; 
        } else {
            errorMessage = `Error status: ${err.status}`;
            if (err.hasOwnProperty('message')) {
                errorMessage += err.message;
            }
        }
     }

    if (errorMessage === '') {
        if (err.hasOwnProperty('error') && err.error.hasOwnProperty('message')) { 
            // if error has status
            errorMessage = `Error: ${err.error.message}`;
        }
     }

    // no errors, then is connection error
    if (errorMessage === '') errorMessage = environment.httpErrors[0].msg; 

    // this.snackBar.open(errorMessage, 'Close', { duration: 5000 }});
    console.error(errorMessage);
    return Observable.throw(errorMessage);
}
Kanso Code
  • 7,479
  • 5
  • 34
  • 49
gildniy
  • 3,528
  • 1
  • 33
  • 23