Lidando com 401s globalmente com Angular

90

Em meu projeto Angular 2, faço chamadas de API de serviços que retornam um Observable. O código de chamada então se inscreve neste observável. Por exemplo:

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

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

Obrigado.


Aqui está o que 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<Response> {

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

        return super.request(url, options);        
    }

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

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

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

A mensagem de erro que estou recebendo é "backend.createConnection is not a function"

PBZ
fonte
1
Acho que isso pode te dar uma pequena
dica

Respostas:

78

Descrição

A melhor solução que encontrei é substituir o de XHRBackendmodo que o status de resposta HTTP 401e 403leve a uma ação específica.

Se você manipular sua autenticação fora de seu aplicativo Angular, poderá forçar uma atualização da página atual de forma que seu mecanismo externo seja acionado. Eu detalho essa 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 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 Httpmódulo.

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

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

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

Outra solução usando roteador e um serviço de autenticação externa é detalhada na essência a seguir por @mrgoos.

Angular pré-2.3.0

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

Ele redireciona para a página atual (mais um parâmetro para obter um URL exclusivo e evitar 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 {
}
Nicolas Henneaux
fonte
2
Obrigado! Descobri meu problema ... Esta linha estava faltando, por isso catch()não foi encontrada. (smh) import "rxjs/add/operator/catch";
hartpdx
1
É possível usar o módulo Roteador para fazer a navegação?
Yuanfei Zhu
1
Ótima solução para empacotar com Auth Guard! 1. O Auth Guard verifica o usuário autorizado (por exemplo, examinando LocalStorage). 2. Na resposta 401/403, você limpa o usuário autorizado para o Guard (por exemplo, removendo os parâmetros correspondentes no LocalStorage). 3. Como nesta fase inicial você não pode acessar o Roteador para encaminhar para a página de login, atualizar a mesma página irá acionar as verificações do Guard, que o encaminharão para a tela de login (e opcionalmente preservarão sua URL inicial, para que você ' será encaminhado para a página solicitada após a autenticação bem-sucedida).
Alex Klaus
1
Ei @NicolasHenneaux - por que você acha que é melhor então substituir http? O único benefício que vejo é que você pode simplesmente colocá-lo como um provedor: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }enquanto ao substituir o Http, você precisa escrever um código mais estranho useFactorye se limitar chamando 'novo' e enviando argumentos específicos. WDYT? Uma referência ao segundo método: adonespitogo.com/articles/angular-2-extending-http-provider
mrgoos
3
@Brett - criei uma essência para ele que deve ajudá-lo: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
82

Angular 4.3+

Com a introdução do HttpClient, veio a capacidade de interceptar facilmente todas as solicitações / respostas. O uso geral de HttpInterceptors está bem documentado , consulte o uso básico e como fornecer o interceptor. Abaixo está um exemplo de um HttpInterceptor que pode lidar com erros 401.

Atualizado para 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
            }
        });
    }
}
Adaga Gilbert Arenas
fonte
1
Isso ainda está funcionando para você? Ontem estava funcionando para mim, mas depois de instalar outros módulos, estou recebendo este erro: next.handle (…) .do não é uma função
Multitut
Eu acho que este deveria ser usado como extensão de classes como http quase sempre é um cheiro
kboom
1
Não se esqueça de adicioná-lo à sua lista de provedores com o HTTP_INTERCEPTORS. Você pode encontrar um exemplo nos documentos
Bruno Peres,
2
Ótimo, mas usar Routeraqui não parece funcionar. Por exemplo, desejo direcionar meus usuários para a página de login quando eles obtiverem um 401-403, mas this.router.navigate(['/login']não está funcionando para mim. Não faz nada
CodyBugstein
Se estiver obtendo ".do não é uma função", adicione import 'rxjs/add/operator/do';depois de importar rxjs.
amoss
19

Como APIs de front-end expiram mais rápido do que leite, com Angular 6+ e RxJS 5.5+, você precisa usar 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);
      })
    );
  }
}

Atualização para Angular 7+ e 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
fonte
Eu recebo error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.quando o .pipeestá lá, sem erros quando removo o.pipe
BlackICE
2
@BlackICE Acho que isso reafirma a primeira frase da minha resposta. Eu atualizei com uma resposta para a versão mais recente.
Saeb Amini
1
No seu ng7 +, o exemplo reqé request- a edição é pequena
demais
12

O que Observablevocê obtém de cada método de solicitação é do tipo Observable<Response>. O Responseobjeto, possui uma statuspropriedade que irá reter o 401IF o servidor retornou aquele código. Portanto, você pode querer recuperá-lo antes de mapeá-lo ou convertê-lo.

Se você quiser evitar fazer essa funcionalidade em cada chamada, pode ter que estender a Httpclasse de Angular 2 e injetar sua própria implementação dela que chama o pai ( super) para a Httpfuncionalidade regular e, em seguida, manipular o401 erro antes de retornar o objeto.

Vejo:

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

Langley
fonte
Portanto, se eu estender o Http, poderei redirecionar para uma rota de "login" de dentro do Http?
pbz
Essa é a teoria. Você terá que injetar o roteador em sua implementação Http para fazer isso.
Langley
Obrigado pela ajuda. Eu atualizei a pergunta com um código de amostra. Provavelmente estou fazendo algo errado (sendo novo no Angular). Alguma ideia do que poderia ser? Obrigado.
pbz
Você está usando os provedores Http padrão, você deve criar seu próprio provedor que resolva para uma instância de sua classe em vez do padrão. Veja: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley
1
@Langley, obrigado. Você está certo: subscribe ((result) => {}, (error) => {console.log (error.status);}. O parâmetro de erro ainda é do tipo Resposta.
abedurftig
9

Angular 4.3+

Para completar a resposta da Adaga Gilbert Arenas :

Se o que você precisa é interceptar qualquer erro, aplique um tratamento a ele e encaminhe-o para baixo na cadeia (e não apenas adicione um efeito colateral .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<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
fonte
9

Para evitar o problema de referência cíclica causado por serviços como "Roteador" sendo injetados em uma classe derivada de Http, deve-se usar o método Injetor pós-construtor. O código a seguir é uma implementação funcional 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 a alteração de nada nos componentes ou serviços já existentes do seu aplicativo.

app.module.ts

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

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

Tutmosis
fonte
8

Em Angular> = 2.3.0, você pode substituir oHTTP módulo 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 ideia geral para mostrar como isso é feito.

mrgoos
fonte
Obrigado por juntar isso. Eu estava recebendo um erro de compilação que dizia "Não é possível encontrar o nome 'Http'" Em app.module.ts, então importei e agora estou recebendo o seguinte erro: "Não é possível instanciar dependência cíclica! Http: no NgModule AppModule"
Bryan
Ei @ Brett- você pode compartilhar seu app.modulecódigo? Obrigado.
mrgoos
Parece ok. Você pode adicionar à essência o HTTP estendido? Além disso, você importa para HTTPoutro lugar?
Mrgoos
Desculpe o atraso. Estou no Angular 2.4 agora e obtendo o mesmo erro. Importo o Http em vários arquivos. Aqui está minha essência atualizada: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Bryan
O mesmo problema aqui ... Parece que esta essência não está funcionando, então talvez devêssemos marcá-la como tal?
Tuthmosis de
2

Angular> 4.3: ErrorHandler para o serviço de 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);
}
Gildniy
fonte