0
votes

Using Spring boot as my backend which send some error response with json body to my frontend angular. I have angular http interceptor sets up to intercept http errors. but i can not get the json error response from the backend.

Opening chrome tools, i can see the response and when i log the http error from my angular interceptor, the error object is null.

return next.handle(request)

      .pipe(tap(event => {
          // 
      }, (err: HttpErrorResponse) => {
        console.log(JSON.stringify(err));
      }));

I expect to be able to access the message property of this response copied from chrome dev tools

{timestamp: "2019-06-24T15:17:41.864+0000", status: 400, error: "Bad Request",…}
error: "Bad Request"
errors: [{codes: ["Min.examinerLogin.phoneNo", "Min.phoneNo", "Min.java.lang.String", "Min"],…}]
message: "Validation failed for object='examinerLogin'. Error count: 1"
path: "/api/public/letters"
status: 400
timestamp: "2019-06-24T15:17:41.864+0000"
trace: "org.springframework.web.bind.MethodArgumentNotVali"

my interceptor code is this:

    import {Injectable} from '@angular/core';
import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpErrorResponse, HttpResponse} from '@angular/common/http';
import { AuthService } from './auth.service';
import {Router} from '@angular/router';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {MessageService} from 'primeng/api';

@Injectable({
  providedIn: 'root'
})

export class TokenInterceptor implements HttpInterceptor {

  constructor(
    private router: Router,
    private authenticationService: AuthService,
    private messageService: MessageService
    ) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(request)

      .pipe(tap(event => {
        if (event instanceof HttpResponse) {
          // do stuff with response if you want
        }
      }, (err: HttpErrorResponse) => {
        console.log(JSON.stringify(err));
        if (err instanceof HttpErrorResponse) {

              this.messageService.add({
                severity: 'error',
                summary: 'Server Error', detail: err.error.message
              });
            }
      }));
  }
}

Update: In my http service call, i modified the headers and change the response type from default JSON to arrayBuffer like so:

    printLetter(letter: Letter) {
    const headers = new HttpHeaders();
    // @ts-ignore
    return this.http.post<any>(this.apiLetter, letter, {headers, responseType: 'arrayBuffer'});
  }

So Angular expects arrayBuffer response instead of JSON error response. Now I wonder if the response type can be of type array such that both JSON and arrayBuffer can be expected

2
You are probably resolving the error as a valid response so you could check your valid response object to check it. I am not sure because I don't know how you resolve your error from your backend. Generally a status 400 will return in the error section of a subscribed observable.Giwrgos Lampadaridis
@GiwrgosLampadaridis thanks for your comment but am using spring boot 5 and the example here is validation error response and its not a valid responseRaines
Mmm have you checked if the interceptor works? Does it get the object sent by the server ? You can just console.log the whole object on the interceptor to check this.Giwrgos Lampadaridis
@GiwrgosLampadaridis the interceptor works. logging the error response, the error.error object is nullRaines
The error.error should be a string though, not an http errorGiwrgos Lampadaridis

2 Answers

2
votes

Do not try to cast err to HttpErrorResponse it will only cause you trouble in this case. Instead cast it to any. Heres the corrected code:

export class TokenInterceptor implements HttpInterceptor {

  constructor(
    private router: Router,
    private authenticationService: AuthService,
    private messageService: MessageService
    ) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    return next.handle(request)

      .pipe(tap(event => {
        if (event instanceof HttpResponse) {
          // do stuff with response if you want
        }
      }, (err: any) => {
        console.log(JSON.stringify(err));
        if (err instanceof HttpErrorResponse) {

              this.messageService.add({
                severity: 'error',
                summary: 'Server Error', detail: err.error.message
              });
            }
      }));
  }
}

EDIT:

Also do not simply send the spring exception error to the client. The client must not know that you are using Spring on the backend. Instead send back a custom error message by using an @ControllerAdvice exceptionhandler class with @ExceptionHandler methods, with each method handling a different error. For example an unauthenticated exception might be handled by a method like this:

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<String> authenticationException(final AuthenticationException e) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    return new ResponseEntity<String>("You need to be logged in to do this", headers, HttpStatus.UNAUTHORIZED);
}

This will create an HttpErrorResponse with status code 401 and a custom message

Edit 2:

Another warning concerning the error object sent by spring boot. I've discovered that Spring isn't very coherent in the way it packages the error message in the error response. Sometimes the error message can indeed be retrieved with error.error.message, but other times the message can only be retrieved by getting it from error.error in such a case it still seems to have the propery message only its value is undefined. Why this is so i don't know but i can recommend everyone to extract the message into a message variable before using the message and to do a check first to see if the error.error object has a property called message and if so, to check whether message is undefined or not. If not, use the value from message if so, get it from error.error.

I know this sounds super hacky but its the only way to get this stuff to work. Not doing this and using a String method on the message property when in fact this value is "undefined" because spring or angular or whatever decided to put the message in error.error instead causes some spooky behavior. For example, this if(errorMessage && errorMessage.message.includes('#redirect')) caused the entire if-else structure to be simply broken off instead of assessing whether the condition is true or false. I confirmed this strange behavior by adding console.log("test) statements between both the if and else brackets. None of the logs were displayed in the browser, furthermore any logs that i place after the if-else structure are not displayed either! All because I simply call .includes on message which is "undefined" according to angular. So no javascript errors are thrown, instead it just mysteriously and silently exits the if-else structure without going through either one of them, plus it does not execute any logs after that. I think it must be a bug in angular or something.

Anyway, like is said: determine whether the error.error object has a message property and if so check whether angular says it has an undefined value or not. You can use this code to retrieve the message.

let errorMessage:any = (Object.prototype.hasOwnProperty.call(error.error, 'message') && error.error.message != undefined) ? error.error.message : error.error;

0
votes

finally got it working...

from this answer here https://stackoverflow.com/a/48502027/1597923

So I added this piece to my error.interceptor.ts file and voila

....
if (err.error instanceof Blob) {
   const reader: FileReader = new FileReader();
   reader.onloadend = () => {
   if (typeof reader.result === 'string') {
       const err1 = JSON.parse(reader.result);
       this.messageService.add({
          severity: 'error',
          summary: err1.error, detail: err1.message
       });
   }
 };
 reader.readAsText(err.error);
            }
....