Глобальная обработка ошибок 401 с помощью Angular

92

В моем проекте Angular 2 я делаю вызовы API из служб, которые возвращают Observable. Затем вызывающий код подписывается на эту наблюдаемую. Например:

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

Допустим, сервер возвращает 401. Как я могу отловить эту ошибку глобально и перенаправить на страницу / компонент входа в систему?

Спасибо.


Вот что у меня есть на данный момент:

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

Я получаю сообщение об ошибке: «backend.createConnection не является функцией».

pbz
источник
1
Думаю, это может дать вам небольшую
подсказку

Ответы:

79

Описание

Лучшее решение, которое я нашел, - переопределить XHRBackendтак, чтобы статус ответа HTTP 401и403 приводил к определенному действию.

Если вы обрабатываете аутентификацию вне приложения Angular, вы можете принудительно обновить текущую страницу, чтобы сработал ваш внешний механизм. Я подробно описываю это решение в реализации ниже.

Вы также можете перенаправить компонент внутри вашего приложения, чтобы ваше приложение Angular не перезагружалось.

Реализация

Угловой> 2.3.0

Благодаря @mrgoos, вот упрощенное решение для angular 2.3.0+ из-за исправления ошибки в angular 2.3.0 (см. Проблему https://github.com/angular/angular/issues/11606 ), расширяющую непосредственно 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<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);
        });
  }
}

Теперь файл модуля содержит только следующего провайдера.

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

Другое решение, использующее маршрутизатор и внешнюю службу аутентификации, подробно описывается в следующей сущности. по @mrgoos.

Угловой до 2.3.0

Следующая реализация работает для Angular 2.2.x FINALиRxJS 5.0.0-beta.12 .

Он перенаправляет на текущую страницу (плюс параметр для получения уникального URL-адреса и предотвращения кеширования), если возвращается HTTP-код 401 или 403.

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

}

со следующим файлом модуля.

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 {
}
Николя Хено
источник
2
Благодарность! Я понял свою проблему ... Мне не хватало этой строки, поэтому catch()она не была найдена. (smh) import "rxjs/add/operator/catch";
hartpdx
1
Можно ли использовать модуль Router для навигации?
Юаньфэй Чжу
1
Отличное решение для пакета с Auth Guard! 1. Auth Guard проверяет авторизованного пользователя (например, просматривая LocalStorage). 2. При ответе 401/403 вы очищаете авторизованного пользователя для Guard (например, удаляя соответствующие параметры в LocalStorage). 3. Поскольку на этом раннем этапе вы не можете получить доступ к маршрутизатору для перенаправления на страницу входа в систему, обновление той же страницы вызовет проверки Guard, которые перенаправят вас на экран входа в систему (и, возможно, сохраните исходный URL-адрес, так что вы: будут перенаправлены на запрошенную страницу после успешной аутентификации).
Alex Klaus,
1
Привет, @NicolasHenneaux, почему ты думаешь, что тогда лучше отказаться http? Единственное преимущество, которое я вижу, это то, что вы можете просто указать его как поставщика: { provide: XHRBackend, useClass: AuthenticationConnectionBackend }при переопределении Http вам нужно писать более неудобный код, например, useFactoryи ограничивать себя, вызывая 'new' и отправляя определенные аргументы. WDYT? Ссылка на 2-й метод: adonespitogo.com/articles/angular-2-exnding-http-provider
mrgoos
3
@Brett - я придумал для этого суть, которая должна вам помочь: gist.github.com/mrgoos/45ab013c2c044691b82d250a7df71e4c
mrgoos
84

Угловой 4.3+

С появлением HttpClient появилась возможность легко перехватывать все запросы / ответы. Общее использование HttpInterceptors хорошо задокументировано , см. Основные принципы использования и способы предоставления перехватчика. Ниже приведен пример HttpInterceptor, который может обрабатывать ошибку 401.

Обновлено для 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
            }
        });
    }
}
Кинжал Гилберта Аренаса
источник
1
Это все еще работает для вас? Вчера это работало у меня, но после установки других модулей я получаю эту ошибку: next.handle (…) .do не является функцией
Multitut
Я думаю, что его следует использовать как расширение классов, например http, почти всегда запах
kboom
1
Не забудьте добавить его в список поставщиков с помощью HTTP_INTERCEPTORS. Вы можете найти пример в документации
Бруно Перес
2
Отлично, но использование Routerздесь, похоже, не работает. Например, я хочу направить своих пользователей на страницу входа в систему, когда они получат 401-403, но this.router.navigate(['/login']у меня это не работает. Это ничего не делает
CodyBugstein
Если вы получаете «.do не является функцией», добавьте import 'rxjs/add/operator/do';после импорта rxjs.
Amoss 08
20

Поскольку интерфейсы API-интерфейсов истекают быстрее, чем молоко, с Angular 6+ и RxJS 5.5+ вам необходимо использовать 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);
      })
    );
  }
}

Обновление для Angular 7+ и 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;
        })
      );
  }
}

Саеб Амини
источник
Я получаю, error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<HttpEvent<any>>'.когда там .pipeнаходится, никаких ошибок при удалении.pipe
BlackICE
2
@BlackICE Полагаю, это подтверждает первое предложение моего ответа. Я обновился с ответом на последнюю версию.
Saeb Amini
1
В вашем примере с ng7 + reqэто на самом деле request- редактирование
слишком
почему мы используем interceprot и почему мы не можем справиться с этим с помощью вызова API входа в систему с помощью метода post
saber tabatabaee yazdi
12

ObservableВы получаете от каждого метода запроса имеет типа Observable<Response>. У Responseобъекта есть statusсвойство, которое будет содержать, 401если сервер вернул этот код. Так что вы можете получить это перед отображением или преобразованием.

Если вы хотите избежать использования этой функции при каждом вызове, вам, возможно, придется расширить Httpкласс Angular 2 и внедрить свою собственную реализацию, которая вызывает parent ( super) для обычных Httpфункций, а затем обрабатывает 401ошибку перед возвратом объекта.

Увидеть:

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

Лэнгли
источник
Итак, если я расширю Http, я смогу перенаправить на маршрут «входа в систему» ​​изнутри Http?
pbz
Это теория. Для этого вам нужно будет встроить маршрутизатор в вашу реализацию Http.
Langley
Спасибо за вашу помощь. Я обновил вопрос образцом кода. Я, наверное, что-то делаю не так (новичок в Angular). Есть идеи, что это может быть? Спасибо.
pbz
Вы используете поставщиков Http по умолчанию, вам необходимо создать своего собственного поставщика, который разрешает экземпляр вашего класса вместо того, который используется по умолчанию. См .: angular.io/docs/ts/latest/api/core/Provider-class.html
Langley,
1
@ Лэнгли, спасибо. Вы правы: subscribe ((result) => {}, (error) => {console.log (error.status);}. Параметр ошибки по-прежнему имеет тип Response.
abedurftig 04
9

Угловой 4.3+

Завершить Кинжал Гилберта Аренаса ответ :

Если вам нужно перехватить любую ошибку, применить к ней обработку и передать ее по цепочке (а не просто добавить побочный эффект с помощью .do), вы можете использовать HttpClient и его перехватчики, чтобы сделать что-то в этом роде:

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'));
        });
    }
}
Старскрим
источник
9

Чтобы избежать проблемы с циклическими ссылками, которая возникает из-за того, что такие службы, как «Маршрутизатор», внедряются в производный класс Http, необходимо использовать метод Injector после конструктора. Следующий код представляет собой рабочую реализацию Http-службы, которая перенаправляет на маршрут входа каждый раз, когда REST API возвращает «Token_Expired». Обратите внимание, что его можно использовать как замену обычному протоколу Http, и поэтому не требуется ничего менять в уже существующих компонентах или службах вашего приложения.

app.module.ts

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

расширенный-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());
  }
}

Тутмос
источник
8

Из Angular> = 2.3.0 вы можете переопределить HTTPмодуль и внедрить свои службы. До версии 2.3.0 вы не могли использовать внедренные сервисы из-за основной ошибки.

Я создал суть, чтобы показать, как это делается.

mrgoos
источник
Спасибо, что собрали это вместе. Я получал ошибку сборки с надписью «Не удается найти имя 'Http'» в app.module.ts, поэтому я импортировал и теперь получаю следующую ошибку: «Не удается создать экземпляр циклической зависимости! Http: в NgModule AppModule»
Брайан
Привет, @ Brett, можешь поделиться своим app.moduleкодом? Спасибо.
mrgoos
Вроде нормально. Можете ли вы добавить к сути расширенный HTTP? Кроме того, вы HTTPкуда-нибудь еще импортируете ?
mrgoos
Извините за задержку. Я сейчас на Angular 2.4 и получаю ту же ошибку. Импортирую Http в несколько файлов. Вот моя обновленная суть: gist.github.com/anonymous/606d092cac5b0eb7f48c9a357cd150c3
Брайан
Здесь та же проблема ... Похоже, суть не работает, может, стоит отметить ее как таковую?
Тутмос
2

Angular> 4.3: ErrorHandler для базовой службы

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);
}
гильдный
источник