Captura de erros em Angular HttpClient

114

Tenho um serviço de dados parecido com este:

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

Se eu obtiver um erro HTTP (ou seja, 404), recebo uma mensagem desagradável do console: ERROR Error: Uncaught (na promessa): [object Object] from core.es5.js Como faço para lidar com isso no meu caso?

Último Tribunal
fonte

Respostas:

231

Você tem algumas opções, dependendo de suas necessidades. Se você quiser tratar os erros por solicitação, adicione um catchà sua solicitação. Se você quiser adicionar uma solução global, use HttpInterceptor.

Abra aqui o êmbolo de demonstração de trabalho para as soluções abaixo.

tl; dr

No caso mais simples, você só precisa adicionar um .catch()ou um .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)
      );

Mas há mais detalhes sobre isso, veja abaixo.


Solução do método (local): registrar o erro e retornar a resposta de fallback

Se precisar lidar com erros em apenas um lugar, você pode usar catche retornar um valor padrão (ou resposta vazia) em vez de falhar completamente. Você também não precisa do .mapapenas para lançar, você pode usar uma função genérica. Fonte: Angular.io - Obtendo detalhes do erro .

Portanto, um .get()método genérico seria 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>();
          });
     }
}

Lidar com o erro permitirá que seu aplicativo continue, mesmo quando o serviço no URL estiver em más condições.

Essa solução por solicitação é boa principalmente quando você deseja retornar uma resposta padrão específica para cada método. Mas se você se preocupa apenas com a exibição de erros (ou tem uma resposta padrão global), a melhor solução é usar um interceptor, conforme descrito abaixo.

Execute o plunker de demonstração de trabalho aqui .


Uso avançado: interceptando todas as solicitações ou respostas

Mais uma vez, o guia Angular.io mostra:

Um dos principais recursos do @angular/common/httpé a interceptação, a capacidade de declarar interceptadores que ficam entre seu aplicativo e o back-end. Quando seu aplicativo faz uma solicitação, os interceptores a transformam antes de enviá-la ao servidor e podem transformar a resposta no caminho de volta antes que seu aplicativo a veja. Isso é útil para tudo, desde autenticação até registro.

Que, claro, pode ser usado para lidar com erros de uma forma muito simples ( plunker de demonstração aqui ):

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

Fornecendo seu interceptor: simplesmente declarar o HttpErrorInterceptoracima não faz com que seu aplicativo o use. Você precisa conectá-lo ao seu módulo de app , fornecendo-o como um interceptor, da seguinte maneira:

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: Se você tem tanto um interceptor de erro e alguns manipulação de erro local, naturalmente, é provável que nenhum tratamento de erros local irá sempre ser acionado, desde que o erro será sempre tratado pelo interceptor antes que ele atinja o tratamento de erros local.

Execute o plunker de demonstração de trabalho aqui .

acdcjunior
fonte
2
bem, se ele quer ser totalmente gosta que ele deixaria seu serviço totalmente clara: return this.httpClient.get<type>(...). e, em seguida, ter catch...algum lugar fora do serviço onde ele realmente consome, porque é onde ele estará construindo um fluxo observável e poderá lidar com isso melhor.
dee zg
1
Eu concordo, talvez uma solução ótima seria ter o Promise<Object>cliente de (o chamador dos DataServicemétodos de) para lidar com o erro. Exemplo: this.dataService.post('url', {...}).then(...).catch((e) => console.log('handle error here instead', e));. Escolha o que for mais claro para você e para os usuários do seu serviço.
acdcjunior
1
Isso não compila: return Observable.of({my: "default value..."}); dá um erro "| ... 'não pode ser atribuído ao tipo' HttpEvent <qualquer> '."
Yakov Fain
1
@YakovFain Se você quiser um valor padrão no interceptor, deve ser a HttpEvent, como a HttpResponse. Assim, por exemplo, você poderia usar: return Observable.of(new HttpResponse({body: [{name: "Default value..."}]}));. Eu atualizei a resposta para deixar este ponto claro. Além disso, criei um plunker de demonstração funcional para mostrar tudo funcionando: plnkr.co/edit/ulFGp4VMzrbaDJeGqc6q?p=preview
acdcjunior
1
@acdcjunior, você é um presente constante :)
Último Tribunal,
67

Deixe-me atualizar a resposta do acdcjunior sobre o uso do HttpInterceptor com os recursos RxJs mais recentes (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
fonte
11
Isso precisa ser mais votado. a resposta do acdcjunior é inutilizável hoje
Paul Kruger
48

Com a chegada da HTTPClientAPI, não apenas a HttpAPI foi substituída, mas uma nova foi adicionada, a HttpInterceptorAPI.

AFAIK um de seus objetivos é adicionar comportamento padrão a todas as solicitações de saída HTTP e respostas de entrada.

Portanto, supondo que você deseja adicionar um comportamento padrão de tratamento de erros , adicionar .catch()todos os métodos http.get / post / etc possíveis é ridiculamente difícil de manter.

Isso pode ser feito da seguinte maneira, como exemplo, usando um 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 {}

Algumas informações extras para OP: Chamar http.get / post / etc sem um tipo forte não é um uso ideal da API. Seu serviço deve ser assim:

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

Retornar Promisesde seus métodos de serviço em vez de Observablesé outra má decisão.

E um conselho extra: se você estiver usando o script TYPE , comece a usar a parte type dele. Você perde uma das maiores vantagens da linguagem: saber o tipo de valor com que está lidando.

Se você quiser, na minha opinião, um bom exemplo de serviço angular, dê uma olhada na essência a seguir .

Jota.Toledo
fonte
Os comentários não são para discussão extensa; esta conversa foi movida para o chat .
deceze
Presumo que isso deva estar this.http.get()etc. e não this.get()etc. em DataService?
displayname
A resposta selecionada parece agora ser mais completa.
Chris Haines
9

Bastante simples (em comparação a como era feito com a API anterior).

Fonte (copie e cole) do guia oficial 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
fonte
9

Para Angular 6+, .catch não funciona diretamente com Observable. Você tem que usar

.pipe(catchError(this.errorHandler))

Código abaixo:

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 obter mais detalhes, consulte o Guia Angular para Http

Udith Indrakantha
fonte
1
Esta é a única resposta que funcionou para mim. Os outros fornecem um erro de: "O tipo 'Observável <desconhecido>' não pode ser atribuído ao tipo 'Observável <HttpEvent <qualquer>>".
Rei Artur Terceiro
5

Exemplo de serviço de tratamento de erros de Angular 8 HttpClient

insira a descrição da imagem aqui

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
fonte
2

Você provavelmente quer algo assim:

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

Depende muito também de como você usa seu serviço, mas este é o caso básico.

dee zg
fonte
1

Seguindo a resposta de @acdcjunior, foi assim que eu o implementei

serviço:

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

chamador:

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
fonte
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
fonte
0

Se você não conseguir detectar erros com qualquer uma das soluções fornecidas aqui, pode ser que o servidor não esteja lidando com as solicitações CORS.

Nesse caso, o Javascript, muito menos o Angular, pode acessar as informações do erro.

Procure por avisos em seu console que incluam CORBou Cross-Origin Read Blocking.

Além disso, a sintaxe mudou para lidar com erros (conforme descrito em todas as outras respostas). Agora você usa operadores que permitem canalização, como:

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

Usando o Interceptor, você pode detectar erros. Abaixo está o 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");
        }
      }
    });
  }
}

Você pode preferir este blog .. dado um exemplo simples para isso.

Prashant M Bhavsar
fonte