Przechwytywanie błędów w Angular HttpClient

114

Mam usługę transmisji danych, która wygląda następująco:

@Injectable()
export class DataService {
    baseUrl = 'http://localhost'
        constructor(
        private httpClient: HttpClient) {
    }
    get(url, params): Promise<Object> {

        return this.sendRequest(this.baseUrl + url, 'get', null, params)
            .map((res) => {
                return res as Object
            })
            .toPromise();
    }
    post(url, body): Promise<Object> {
        return this.sendRequest(this.baseUrl + url, 'post', body)
            .map((res) => {
                return res as Object
            })
            .toPromise();
    }
    patch(url, body): Promise<Object> {
        return this.sendRequest(this.baseUrl + url, 'patch', body)
            .map((res) => {
                return res as Object
            })
            .toPromise();
    }
    sendRequest(url, type, body, params = null): Observable<any> {
        return this.httpClient[type](url, { params: params }, body)
    }
}

Jeśli otrzymuję błąd HTTP (np. 404), otrzymuję nieprzyjemny komunikat konsoli: ERROR Błąd: Uncatcht (w obietnicy): [object Object] z core.es5.js Jak sobie z tym poradzić w moim przypadku?

LastTribunal
źródło

Odpowiedzi:

231

Masz kilka opcji, w zależności od potrzeb. Jeśli chcesz obsługiwać błędy na podstawie poszczególnych żądań, dodaj catchdo żądania. Jeśli chcesz dodać globalne rozwiązanie, użyj HttpInterceptor.

Otwórz tutaj działający plunker demonstracyjny dla poniższych rozwiązań.

tl; dr

W najprostszym przypadku wystarczy dodać a .catch()lub a .subscribe(), na przykład:

import 'rxjs/add/operator/catch'; // don't forget this, or you'll get a runtime error
this.httpClient
      .get("data-url")
      .catch((err: HttpErrorResponse) => {
        // simple logging, but you can do a lot more, see below
        console.error('An error occurred:', err.error);
      });

// or
this.httpClient
      .get("data-url")
      .subscribe(
        data => console.log('success', data),
        error => console.log('oops', error)
      );

Ale jest więcej szczegółów na ten temat, patrz poniżej.


Rozwiązanie metody (lokalne): błąd dziennika i zwrot odpowiedzi rezerwowej

Jeśli potrzebujesz obsługiwać błędy tylko w jednym miejscu, możesz użyć catchi zwrócić wartość domyślną (lub pustą odpowiedź) zamiast całkowicie zawieść. Nie potrzebujesz też .maptylko do rzutowania, możesz użyć funkcji ogólnej. Źródło: Angular.io - Pobieranie szczegółów błędu .

Tak więc ogólna .get()metoda wyglądałaby tak:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/empty';
import 'rxjs/add/operator/retry'; // don't forget the imports

@Injectable()
export class DataService {
    baseUrl = 'http://localhost';
    constructor(private httpClient: HttpClient) { }

    // notice the <T>, making the method generic
    get<T>(url, params): Observable<T> {
      return this.httpClient
          .get<T>(this.baseUrl + url, {params})
          .retry(3) // optionally add the retry
          .catch((err: HttpErrorResponse) => {

            if (err.error instanceof Error) {
              // A client-side or network error occurred. Handle it accordingly.
              console.error('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.error(`Backend returned code ${err.status}, body was: ${err.error}`);
            }

            // ...optionally return a default fallback value so app can continue (pick one)
            // which could be a default value
            // return Observable.of<any>({my: "default value..."});
            // or simply an empty observable
            return Observable.empty<T>();
          });
     }
}

Obsługa błędu umożliwi kontynuowanie działania aplikacji nawet wtedy, gdy usługa pod adresem URL jest w złym stanie.

To rozwiązanie na żądanie jest dobre głównie wtedy, gdy chcesz zwrócić określoną domyślną odpowiedź dla każdej metody. Ale jeśli zależy Ci tylko na wyświetlaniu błędów (lub masz globalną odpowiedź domyślną), lepszym rozwiązaniem jest użycie przechwytywacza, jak opisano poniżej.

Uruchom tutaj działającego demo plunkera .


Użycie zaawansowane: przechwytywanie wszystkich żądań lub odpowiedzi

Po raz kolejny przewodnik Angular.io pokazuje:

Główną cechą @angular/common/httpjest przechwytywanie, czyli możliwość deklarowania przechwytywaczy, które znajdują się pomiędzy aplikacją a zapleczem. Kiedy aplikacja wysyła żądanie, elementy przechwytujące przekształcają je przed wysłaniem do serwera, a przechwytywacze mogą przekształcić odpowiedź w drodze powrotnej, zanim aplikacja ją zobaczy. Jest to przydatne do wszystkiego, od uwierzytelniania po rejestrowanie.

Które oczywiście można wykorzystać do obsługi błędów w bardzo prosty sposób ( tutaj demo plunker ):

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse,
         HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/empty';
import 'rxjs/add/operator/retry'; // don't forget the imports

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .catch((err: HttpErrorResponse) => {

        if (err.error instanceof Error) {
          // A client-side or network error occurred. Handle it accordingly.
          console.error('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.error(`Backend returned code ${err.status}, body was: ${err.error}`);
        }

        // ...optionally return a default fallback value so app can continue (pick one)
        // which could be a default value (which has to be a HttpResponse here)
        // return Observable.of(new HttpResponse({body: [{name: "Default value..."}]}));
        // or simply an empty observable
        return Observable.empty<HttpEvent<any>>();
      });
  }
}

Zapewnienie przechwytywacza: samo zadeklarowanie HttpErrorInterceptorpowyższego nie spowoduje, że aplikacja będzie go używać. Musisz połączyć go w module aplikacji, dostarczając go jako przechwytywacz w następujący sposób:

import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpErrorInterceptor } from './path/http-error.interceptor';

@NgModule({
  ...
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: HttpErrorInterceptor,
    multi: true,
  }],
  ...
})
export class AppModule {}

Uwaga: Jeśli masz zarówno przechwytywacz błędów, jak i pewną lokalną obsługę błędów, jest oczywiście prawdopodobne, że żadna lokalna obsługa błędów nigdy nie zostanie wyzwolona, ​​ponieważ błąd będzie zawsze obsługiwany przez przechwytywacz, zanim dotrze do lokalnej obsługi błędów.

Uruchom tutaj działającego demo plunkera .

acdcjunior
źródło
2
dobrze, jeśli chce być w pełni ochotę on odejdzie służbę pełni jasny: return this.httpClient.get<type>(...). a potem mieć catch...gdzieś poza usługą, gdzie faktycznie to konsumuje, ponieważ tam będzie budował przepływ obserwowalny i może sobie z tym najlepiej poradzić.
dee zg
1
Zgadzam się, być może optymalnym rozwiązaniem byłoby posiadanie Promise<Object>klienta (wywołującego DataServicemetody) do obsługi błędu. Przykład: this.dataService.post('url', {...}).then(...).catch((e) => console.log('handle error here instead', e));. Wybierz to, co jest bardziej zrozumiałe dla Ciebie i użytkowników Twojej usługi.
acdcjunior
1
To się nie kompiluje: return Observable.of({my: "default value..."}); generuje błąd „| ...” nie można przypisać do typu „HttpEvent <any>”. "
Yakov Fain
1
@YakovFain Jeśli chcesz mieć domyślną wartość w przechwytywaczu, musi to być a HttpEvent, na przykład a HttpResponse. Tak więc, na przykład, można użyć: return Observable.of(new HttpResponse({body: [{name: "Default value..."}]}));. Zaktualizowałem odpowiedź, aby wyjaśnić tę kwestię. Stworzyłem również działający demo plunker, aby pokazać wszystko działające: plnkr.co/edit/ulFGp4VMzrbaDJeGqc6q?p=preview
acdcjunior
1
@acdcjunior, jesteś prezentem, który ciągle daje :)
LastTribunal Kwietnia
67

Proszę pozwolić mi zaktualizować odpowiedź acdcjunior dotyczącą używania HttpInterceptor z najnowszymi funkcjami RxJs (v.6).

import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpErrorResponse,
  HttpHandler,
  HttpEvent,
  HttpResponse
} from '@angular/common/http';

import { Observable, EMPTY, throwError, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.error instanceof Error) {
          // A client-side or network error occurred. Handle it accordingly.
          console.error('An error occurred:', error.error.message);
        } else {
          // The backend returned an unsuccessful response code.
          // The response body may contain clues as to what went wrong,
          console.error(`Backend returned code ${error.status}, body was: ${error.error}`);
        }

        // If you want to return a new response:
        //return of(new HttpResponse({body: [{name: "Default value..."}]}));

        // If you want to return the error on the upper level:
        //return throwError(error);

        // or just return nothing:
        return EMPTY;
      })
    );
  }
}
MegaCasper
źródło
11
To musi uzyskać więcej głosów. Odpowiedź acdcjunior jest na dzień dzisiejszy bezużyteczna
Paul Kruger
48

Wraz z pojawieniem się HTTPClientAPI, nie tylko został wymieniony HttpAPI, ale dodano nowy, HttpInterceptorAPI.

AFAIK jednym z jego celów jest dodanie domyślnego zachowania do wszystkich wychodzących żądań HTTP i odpowiedzi przychodzących.

Zakładając więc, że chcesz dodać domyślne zachowanie obsługi błędów , dodanie .catch()do wszystkich możliwych metod http.get / post / etc jest śmiesznie trudne w utrzymaniu.

Można to zrobić w następujący sposób, na przykład, używając HttpInterceptor:

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

/**
 * Intercepts the HTTP responses, and in case that an error/exception is thrown, handles it
 * and extract the relevant information of it.
 */
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    /**
     * Intercepts an outgoing HTTP request, executes it and handles any error that could be triggered in execution.
     * @see HttpInterceptor
     * @param req the outgoing HTTP request
     * @param next a HTTP request handler
     */
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req)
            .catch(errorResponse => {
                let errMsg: string;
                if (errorResponse instanceof HttpErrorResponse) {
                    const err = errorResponse.message || JSON.stringify(errorResponse.error);
                    errMsg = `${errorResponse.status} - ${errorResponse.statusText || ''} Details: ${err}`;
                } else {
                    errMsg = errorResponse.message ? errorResponse.message : errorResponse.toString();
                }
                return _throw(errMsg);
            });
    }
}

/**
 * Provider POJO for the interceptor
 */
export const ErrorInterceptorProvider = {
    provide: HTTP_INTERCEPTORS,
    useClass: ErrorInterceptor,
    multi: true,
};

// app.module.ts

import { ErrorInterceptorProvider } from 'somewhere/in/your/src/folder';

@NgModule({
   ...
   providers: [
    ...
    ErrorInterceptorProvider,
    ....
   ],
   ...
})
export class AppModule {}

Dodatkowe informacje dla OP: Wywołanie http.get / post / etc bez silnego typu nie jest optymalnym wykorzystaniem API. Twoja usługa powinna wyglądać następująco:

// These interfaces could be somewhere else in your src folder, not necessarily in your service file
export interface FooPost {
 // Define the form of the object in JSON format that your 
 // expect from the backend on post
}

export interface FooPatch {
 // Define the form of the object in JSON format that your 
 // expect from the backend on patch
}

export interface FooGet {
 // Define the form of the object in JSON format that your 
 // expect from the backend on get
}

@Injectable()
export class DataService {
    baseUrl = 'http://localhost'
    constructor(
        private http: HttpClient) {
    }

    get(url, params): Observable<FooGet> {

        return this.http.get<FooGet>(this.baseUrl + url, params);
    }

    post(url, body): Observable<FooPost> {
        return this.http.post<FooPost>(this.baseUrl + url, body);
    }

    patch(url, body): Observable<FooPatch> {
        return this.http.patch<FooPatch>(this.baseUrl + url, body);
    }
}

Powrót Promisesz metod serwisu zamiast z nich Observablesto kolejna zła decyzja.

I dodatkowa rada: jeśli używasz skryptu TYPE , zacznij używać jego części typu. Tracisz jedną z największych zalet języka: znać rodzaj wartości, z którą masz do czynienia.

Jeśli chcesz, moim zdaniem, dobry przykład usługi kątowej, spójrz na następującą istotę .

Jota.Toledo
źródło
Komentarze nie służą do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
deceze
Zakładam, że to powinno być this.http.get()itd., A nie this.get()itd. W DataService?
nazwa wyświetlana
Wybrana odpowiedź wydaje się teraz bardziej kompletna.
Chris Haines,
9

Dość proste (w porównaniu z tym, jak zostało to zrobione z poprzednim API).

Źródło z (skopiuj i wklej) oficjalnego przewodnika Angular

 http
  .get<ItemsResponse>('/api/items')
  .subscribe(
    // Successful responses call the first callback.
    data => {...},
    // Errors will call this callback instead:
    err => {
      console.log('Something went wrong!');
    }
  );
Tomer
źródło
9

W przypadku Angular 6+ .catch nie działa bezpośrednio z Observable. Musisz użyć

.pipe(catchError(this.errorHandler))

Poniżej kod:

import { IEmployee } from './interfaces/employee';
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class EmployeeService {

  private url = '/assets/data/employee.json';

  constructor(private http: HttpClient) { }

  getEmployees(): Observable<IEmployee[]> {
    return this.http.get<IEmployee[]>(this.url)
                    .pipe(catchError(this.errorHandler));  // catch error
  }

  /** Error Handling method */

  errorHandler(error: HttpErrorResponse) {
    if (error.error instanceof ErrorEvent) {
      // A client-side or network error occurred. Handle it accordingly.
      console.error('An error occurred:', error.error.message);
    } else {
      // The backend returned an unsuccessful response code.
      // The response body may contain clues as to what went wrong,
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    // return an observable with a user-facing error message
    return throwError(
      'Something bad happened; please try again later.');
  }
}

Aby uzyskać więcej informacji, zapoznaj się z przewodnikiem Angular Guide for Http

Udith Indrakantha
źródło
1
To jedyna odpowiedź, która mi pomogła. Pozostałe podają błąd: „Typ 'Observable <unknown>' nie może zostać przypisany do typu 'Observable <HttpEvent <any>>”.
Król Artur Trzeci
5

Przykład usługi obsługi błędów HttpClient w Angular 8

wprowadź opis obrazu tutaj

api.service.ts

    import { Injectable } from '@angular/core';
    import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
    import { Student } from '../model/student';
    import { Observable, throwError } from 'rxjs';
    import { retry, catchError } from 'rxjs/operators';

    @Injectable({
      providedIn: 'root'
    })
    export class ApiService {

      // API path
      base_path = 'http://localhost:3000/students';

      constructor(private http: HttpClient) { }

      // Http Options
      httpOptions = {
        headers: new HttpHeaders({
          'Content-Type': 'application/json'
        })
      }

      // Handle API errors
      handleError(error: HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          // A client-side or network error occurred. Handle it accordingly.
          console.error('An error occurred:', error.error.message);
        } else {
          // The backend returned an unsuccessful response code.
          // The response body may contain clues as to what went wrong,
          console.error(
            `Backend returned code ${error.status}, ` +
            `body was: ${error.error}`);
        }
        // return an observable with a user-facing error message
        return throwError(
          'Something bad happened; please try again later.');
      };


      // Create a new item
      createItem(item): Observable<Student> {
        return this.http
          .post<Student>(this.base_path, JSON.stringify(item), this.httpOptions)
          .pipe(
            retry(2),
            catchError(this.handleError)
          )
      }

     ........
     ........

    }
Code Spy
źródło
2

Prawdopodobnie chcesz mieć coś takiego:

this.sendRequest(...)
.map(...)
.catch((err) => {
//handle your error here
})

W dużej mierze zależy to również od tego, jak korzystasz z usług, ale jest to podstawowy przypadek.

dee zg
źródło
1

Po odpowiedzi @acdcjunior zaimplementowałem ją w ten sposób

usługa:

  get(url, params): Promise<Object> {

            return this.sendRequest(this.baseUrl + url, 'get', null, params)
                .map((res) => {
                    return res as Object
                }).catch((e) => {
                    return Observable.of(e);
                })
                .toPromise();
        }

gość:

this.dataService.get(baseUrl, params)
            .then((object) => {
                if(object['name'] === 'HttpErrorResponse') {
                            this.error = true;
                           //or any handle
                } else {
                    this.myObj = object as MyClass 
                }
           });
LastTribunal
źródło
1

import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

const PASSENGER_API = 'api/passengers';

getPassengers(): Observable<Passenger[]> {
  return this.http
    .get<Passenger[]>(PASSENGER_API)
    .pipe(catchError((error: HttpErrorResponse) => throwError(error)));
}
siła słońca
źródło
0

Jeśli nie możesz wychwycić błędów w żadnym z przedstawionych tutaj rozwiązań, może to oznaczać, że serwer nie obsługuje żądań CORS.

W takim przypadku Javascript, a tym bardziej Angular, może uzyskać dostęp do informacji o błędzie.

Poszukaj ostrzeżeń w konsoli, które obejmują CORBlub Cross-Origin Read Blocking.

Zmieniła się również składnia obsługi błędów (jak opisano w każdej innej odpowiedzi). Teraz używasz operatorów z potokami, jak na przykład:

this.service.requestsMyInfo(payload).pipe(
    catcheError(err => {
        // handle the error here.
    })
);
Kevin Beal
źródło
0

Używając Interceptora, możesz złapać błąd. Poniżej kod:

@Injectable()
export class ResponseInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    //Get Auth Token from Service which we want to pass thr service call
    const authToken: any = `Bearer ${sessionStorage.getItem('jwtToken')}`
    // Clone the service request and alter original headers with auth token.
    const authReq = req.clone({
      headers: req.headers.set('Content-Type', 'application/json').set('Authorization', authToken)
    });

    const authReq = req.clone({ setHeaders: { 'Authorization': authToken, 'Content-Type': 'application/json'} });

    // Send cloned request with header to the next handler.
    return next.handle(authReq).do((event: HttpEvent<any>) => {
      if (event instanceof HttpResponse) {
        console.log("Service Response thr Interceptor");
      }
    }, (err: any) => {
      if (err instanceof HttpErrorResponse) {
        console.log("err.status", err);
        if (err.status === 401 || err.status === 403) {
          location.href = '/login';
          console.log("Unauthorized Request - In case of Auth Token Expired");
        }
      }
    });
  }
}

Możesz preferować tego bloga .. mając na to prosty przykład.

Prashant M Bhavsar
źródło