Manipulando 401s globalmente com o Angular

No meu projeto Angular 2, faço chamadas de API de serviços que retornam um Observable. O código de chamada, em seguida, assina este observável. Por exemplo:

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

Digamos que o servidor retorne um 401. Como posso detectar esse erro globalmente e redirect para uma página / componente de login?

Obrigado.


Aqui está o que eu tenho até agora:

// 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 { console.log('request...'); return super.request(url, options); } get(url: string, options?: RequestOptionsArgs): Observable { console.log('get...'); return super.get(url, options); } } 

A mensagem de erro que estou recebendo é “backend.createConnection não é uma function”

Descrição

A melhor solução que encontrei é sobrepor o XHRBackend tal forma que o status de resposta HTTP 401 e 403 leva a uma ação específica.

Se você mantiver sua autenticação fora do seu aplicativo Angular, poderá forçar uma atualização da página atual, de modo que seu mecanismo externo seja acionado. Eu detalho esta solução na implementação abaixo.

Você também pode encaminhar para um componente dentro de seu aplicativo, de forma que seu aplicativo Angular não seja recarregado.

Implementação

Angular> 2.3.0

Graças a @mrgoos, aqui está uma solução simplificada para o angular 2.3.0+ devido a uma correção de bug no angular 2.3.0 (consulte o problema https://github.com/angular/angular/issues/11606 ) estendendo diretamente o módulo Http .

 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 { 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); }); } } 

O arquivo de módulo agora contém apenas o seguinte provedor.

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

Outra solução usando o Roteador e um serviço de autenticação externa é detalhada na seguinte essência do @mrgoos.

Angular pré-2.3.0

A implementação a seguir funciona para o Angular 2.2.x FINAL e o RxJS 5.0.0-beta.12 .

Ele redireciona para a página atual (mais um parâmetro para obter uma URL exclusiva e evita o armazenamento em cache) se um código HTTP 401 ou 403 for retornado.

 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; } } 

com o seguinte arquivo de módulo.

 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 { } 

Angular 4.3+

Com a introdução do HttpClient surgiu a capacidade de interceptar facilmente todos os pedidos / respostas. O uso geral do HttpInterceptors está bem documentado , veja o uso básico e como fornecer o interceptador. Abaixo está um exemplo de um HttpInterceptor que pode manipular erros 401.

 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, next: HttpHandler): Observable> { return next.handle(req).do(event => {}, err => { if (err instanceof HttpErrorResponse && err.status == 401) { // handle 401 errors } }); } } 

O Observable obtido de cada método de solicitação é do tipo Observable . O object Response possui uma propriedade de status que conterá o 401 IF se o servidor retornou esse código. Então você pode querer recuperar isso antes de mapeá-lo ou convertê-lo.

Se você quiser evitar fazer essa funcionalidade em cada chamada, você pode ter que estender a class Http do Angular 2 e injetar sua própria implementação que chame o pai ( super ) para a funcionalidade Http regular e então manipular o erro 401 antes de retornar o object.

Vejo:

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

De Angular> = 2.3.0 você pode replace o módulo HTTP e injetar seus serviços. Antes da versão 2.3.0, você não podia usar seus serviços injetados devido a um bug principal.

Eu criei uma essência para mostrar como é feito.

Angular 4.3+

Para completar a resposta do The Gilbert Arenas Dagger :

Se o que você precisa é interceptar qualquer erro, aplicar um tratamento a ele e encaminhá-lo para baixo (e não apenas adicionar um efeito colateral com .do ), você pode usar o HttpClient e seus interceptadores para fazer algo do tipo:

 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, next: HttpHandler): Observable> { // 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')); }); } } 

Para evitar o problema de referência cíclica causado por ter serviços como “Roteador” sendo injetados em uma class derivada de HTTP, deve-se usar o método Injector pós-construtor. O código a seguir é uma implementação em funcionamento de um serviço HTTP que redireciona para a rota de Login sempre que uma API REST retorna “Token_Expired”. Observe que ele pode ser usado como uma substituição para o Http normal e, como tal, não requer alterações nos componentes ou serviços já existentes do seu aplicativo.

app.module.ts

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

Angular> 4.3: ErrorHandler para o serviço base

 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); }