Detectando errores en Angular HttpClient

114

Tengo un servicio de datos que se parece a esto:

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

Si obtengo un error HTTP (es decir, 404), aparece un mensaje de consola desagradable: ERROR Error: No detectado (en promesa): [objeto Objeto] de core.es5.js ¿Cómo lo manejo en mi caso?

Último Tribunal
fuente

Respuestas:

231

Tienes algunas opciones, según tus necesidades. Si desea manejar los errores por solicitud, agregue a catcha su solicitud. Si desea agregar una solución global, use HttpInterceptor.

Abra aquí el plunker de demostración en funcionamiento para las siguientes soluciones.

tl; dr

En el caso más simple, solo necesitará agregar una .catch()o una .subscribe(), como:

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

Pero hay más detalles sobre esto, ver más abajo.


Solución de método (local): error de registro y respuesta de retorno de retorno

Si necesita manejar errores en un solo lugar, puede usar catchy devolver un valor predeterminado (o una respuesta vacía) en lugar de fallar por completo. Tampoco necesitas .mapsolo para emitir, puedes usar una función genérica. Fuente: Angular.io - Obteniendo detalles del error .

Entonces, un .get()método genérico sería como:

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

Manejar el error permitirá que la aplicación continúe incluso cuando el servicio en la URL esté en malas condiciones.

Esta solución por solicitud es buena principalmente cuando desea devolver una respuesta predeterminada específica para cada método. Pero si solo le importa la visualización de errores (o tiene una respuesta predeterminada global), la mejor solución es usar un interceptor, como se describe a continuación.

Ejecute el plunker de demostración en funcionamiento aquí .


Uso avanzado: interceptar todas las solicitudes o respuestas

Una vez más, la guía Angular.io muestra:

Una característica importante de @angular/common/httpes la interceptación, la capacidad de declarar interceptores que se encuentran entre su aplicación y el backend. Cuando su aplicación realiza una solicitud, los interceptores la transforman antes de enviarla al servidor, y los interceptores pueden transformar la respuesta en su camino de regreso antes de que su aplicación la vea. Esto es útil para todo, desde la autenticación hasta el registro.

Que, por supuesto, se puede usar para manejar errores de una manera muy simple ( demo plunker aquí ):

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

Proporcionar su interceptor: simplemente declarar lo HttpErrorInterceptoranterior no hace que su aplicación lo use. Debe conectarlo en el módulo de su aplicación proporcionándolo como un interceptor, de la siguiente manera:

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

Nota: Si tiene tanto un interceptor de errores como un manejo de errores local, naturalmente, es probable que nunca se active el manejo de errores local, ya que el interceptor siempre manejará el error antes de que llegue al manejo de errores local.

Ejecute el plunker de demostración en funcionamiento aquí .

acdcjunior
fuente
2
así, si él quiere ser totalmente apetece dejaría su servicio totalmente claro: return this.httpClient.get<type>(...). y luego tener un catch...lugar fuera del servicio donde realmente lo consume porque ahí es donde construirá el flujo observable y podrá manejarlo mejor.
dee zg
1
Estoy de acuerdo, tal vez una solución óptima sería tener el Promise<Object>cliente de (la persona que llama a los DataServicemétodos de) para manejar el error. Ejemplo: this.dataService.post('url', {...}).then(...).catch((e) => console.log('handle error here instead', e));. Elija lo que sea más claro para usted y los usuarios de su servicio.
Acdcjunior
1
Esto no se compila: return Observable.of({my: "default value..."}); da un error "| ... 'no se puede asignar al tipo' HttpEvent <any> '."
Yakov Fain
1
@YakovFain Si desea un valor predeterminado en el interceptor, debe ser un HttpEvent, como un HttpResponse. Así, por ejemplo, se puede utilizar: return Observable.of(new HttpResponse({body: [{name: "Default value..."}]}));. He actualizado la respuesta para aclarar este punto. Además, creé un plunker de demostración en funcionamiento para mostrar que todo funciona: plnkr.co/edit/ulFGp4VMzrbaDJeGqc6q?p=preview
acdcjunior
1
@acdcjunior, eres un regalo que sigue dando :)
Último Tribunal
67

Permítanme por favor, actualice la acdcjunior respuesta 's sobre el uso de HttpInterceptor con las últimas características 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
fuente
11
Esto necesita recibir más votos a favor. La respuesta de acdcjunior no se puede utilizar a partir de hoy
Paul Kruger
48

Con la llegada de la HTTPClientAPI, no solo se Httpreemplazó la API, sino que se agregó una nueva, la HttpInterceptorAPI.

AFAIK uno de sus objetivos es agregar un comportamiento predeterminado a todas las solicitudes salientes HTTP y respuestas entrantes.

Entonces, suponiendo que desea agregar un comportamiento de manejo de errores predeterminado , agregue.catch() a todos sus posibles métodos http.get / post / etc es ridículamente difícil de mantener.

Esto se podría hacer de la siguiente manera como ejemplo usando a 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 {}

Alguna información adicional para OP: llamar a http.get / post / etc sin un tipo fuerte no es un uso óptimo de la API. Su servicio debería verse así:

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

Volver Promisesde sus métodos de servicio en lugar de Observableses otra mala decisión.

Y un consejo adicional: si está utilizando el script TYPE , empiece a utilizar la parte del tipo. Pierdes una de las mayores ventajas del lenguaje: conocer el tipo de valor con el que estás lidiando.

Si desea, en mi opinión, un buen ejemplo de un servicio angular, eche un vistazo a la siguiente esencia .

Jota.Toledo
fuente
Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
diciembre
Asumo que esto debe ser this.http.get()etc, y no this.get(), etc., en DataService?
displayname
La respuesta seleccionada parece ahora más completa.
Chris Haines
9

Bastante sencillo (en comparación con cómo se hizo con la API anterior).

Fuente de (copiar y pegar) la guía oficial de 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
fuente
9

Para Angular 6+, .catch no funciona directamente con Observable. Tienes que usar

.pipe(catchError(this.errorHandler))

Abajo código:

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

Para obtener más detalles, consulte la Guía angular para Http

Udith Indrakantha
fuente
1
Esta es la única respuesta que funcionó para mí. Los otros dan un error de: "El tipo 'Observable <desconocido>' no se puede asignar al tipo 'Observable <HttpEvent <cualquier>>".
Rey Arturo III
5

Ejemplo de servicio de manejo de errores de Angular 8 HttpClient

ingrese la descripción de la imagen aquí

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

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

    }
Código espía
fuente
2

Probablemente quieras tener algo como esto:

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

Depende en gran medida también de cómo utilice su servicio, pero este es el caso básico.

dee zg
fuente
1

Siguiendo la respuesta de @acdcjunior, así es como lo implementé

Servicio:

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

llamador:

this.dataService.get(baseUrl, params)
            .then((object) => {
                if(object['name'] === 'HttpErrorResponse') {
                            this.error = true;
                           //or any handle
                } else {
                    this.myObj = object as MyClass 
                }
           });
Último Tribunal
fuente
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)));
}
sol sreng
fuente
0

Si no puede detectar errores con alguna de las soluciones proporcionadas aquí, es posible que el servidor no esté manejando las solicitudes CORS.

En ese caso, Javascript, mucho menos Angular, puede acceder a la información del error.

Busque advertencias en su consola que incluyan CORBo Cross-Origin Read Blocking.

Además, la sintaxis ha cambiado para manejar errores (como se describe en todas las demás respuestas). Ahora usa operadores capaces de canalizar, así:

this.service.requestsMyInfo(payload).pipe(
    catcheError(err => {
        // handle the error here.
    })
);
Kevin Beal
fuente
0

Al usar Interceptor, puede detectar errores. A continuación se muestra el código:

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

Puede preferir este blog ... dado un ejemplo sencillo.

Prashant M Bhavsar
fuente