18

In Spring 4.x, if you use a @RestControllerAdvise (or @ControllerAdvice) that extends ResponseEntityExceptionHandler, the default exception handling with nice and informative JSON response bodies, is no longer returned by default for arguments marked as @Valid.

How do you get the default JSON bodies to be returned while using a ResponseEntityExceptionHandler based @RestControllerAdvice?

The following is a simple, yet full example describing this question. Using these classes:

@RestController
class CarsController {

  @PostMapping("/cars")
  public void createCar(@RequestBody @Valid Car car) {
    System.out.println("Creating " + car);
    throw new WhateverException();
  }

  @ExceptionHandler(WhateverException.class)
  @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
  public void handleWhateverException(){
    System.out.println("Handling a WhateverException.");
  }

}

class Car {

  @NotNull
  private String make;

  @NotNull
  private String model;

  ...getter/setters omitted for brevity...
}

class WhateverException extends RuntimeException {}

If you submit a POST to /cars with

{
    "make": "BMW"
}

It responds with a 400 and the following body:

{
  "timestamp": 1491020374642,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "errors": [
    {
      "codes": [
        "NotNull.car.model",
        "NotNull.model",
        "NotNull.java.lang.String",
        "NotNull"
      ],
      "arguments": [
        {
          "codes": [
            "car.model",
            "model"
          ],
          "arguments": null,
          "defaultMessage": "model",
          "code": "model"
        }
      ],
      "defaultMessage": "may not be null",
      "objectName": "car",
      "field": "model",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotNull"
    }
  ],
  "message": "Validation failed for object='car'. Error count: 1",
  "path": "/cars"
}

However if you move the exception handling method to it's own class marked @RestControllerAdvice, which extends from ResponseEntityExceptionHandler such as the following:

@RestControllerAdvice
class RestExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(WhateverException.class)
  @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
  public void handleWhateverException(WhateverException e, HttpServletRequest httpServletRequest) {
    System.out.println("Handling a WhateverException.");
  }

}

You'll get a 400 with an empty body, which is caused by ResponseEntityExceptionHandler providing a method (handleMethodArgumentNotValid(..)), which builds a response where the body is null.

How would you alter this @RestControllerAdvice class to trigger the original handling that occurs, which provides a JSON body describing why the submitted request is invalid?

Ali Dehghani
  • 46,221
  • 15
  • 164
  • 151
peterl
  • 1,881
  • 4
  • 21
  • 34
  • Is this how your `RestExceptionHandler` looks like in reality? Did you truncated the real version for demo purposes? – Ali Dehghani Apr 01 '17 at 09:56
  • I of course can't post my companies code, so this is simply a contrived example to illustrate the question only. – peterl Apr 01 '17 at 22:03
  • Got the same problem. If you already solved it or found a different solution it can be helpful for me :) – Eyal Apr 13 '17 at 16:07
  • No, I have not solved this. I had to abandon Spring's built-in handling because of this situation. It'd be great if someone did solve this as I'd love to go back to using it. – peterl Apr 26 '17 at 02:00

4 Answers4

5

You can emulate spring's behaviour overriding in your @ControllerAdvice extending ResponseEntityExceptionHandler:

@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
                             HttpHeaders headers, HttpStatus status, WebRequest request) {
    if (body == null) {
        body = ImmutableMap.builder()
                   .put("timestamp", LocalDateTime.now().atZone(ZoneId.systemDefault()).toEpochSecond())
                   .put("status", status.value())
                   .put("error", status.getReasonPhrase())
                   .put("message", ex.getMessage())
                   .put("exception", ex.getClass().getSimpleName())  // can show FQCN like spring with getName()
                   .put("path", ((ServletWebRequest)request).getRequest().getRequestURI())
                   .build();
    }
    return super.handleExceptionInternal(ex, body, headers, status, request);
}

* ImmutableMap needs guava.

**LocalDateTime needs java8.

laffuste
  • 16,287
  • 8
  • 84
  • 91
  • 1
    The best idea I've seen so far. Thx ! – Klem231188 Mar 09 '21 at 15:13
  • Where did this code come from? Is there a way to see the original Spring implementation that builds the body to ensure this code is up-to-date? Or, even more preferably, just call the default spring handler directly from my override of it? – Dan Bechard Jun 07 '23 at 21:27
4

How about this?

   @RestControllerAdvice
    class RestExceptionHandler {

      @ExceptionHandler(org.springframework.validation.BindException.class)
      public ResponseEntity<String> handleBindException(org.springframework.validation.BindException e) {
         return ResponseEntity.badRequest().body(e.getMessage());
      }
   }

when @Valid fails, it throws BindException. Which you can handle it like this. or you can just do throw e which would give you the exact same response as was thrown earlier

pvpkiran
  • 25,582
  • 8
  • 87
  • 134
2

One approach is that worked for me is to not extend ResponseEntityExceptionHandler

So the result class would look like:

@RestControllerAdvice
class RestExceptionHandler {

  @ExceptionHandler(WhateverException.class)
  @ResponseStatus(HttpStatus.EXPECTATION_FAILED)
  public ResponseEntity<String> handleWhateverException(WhateverException e, HttpServletRequest httpServletRequest) {
    System.out.println("Handling a WhateverException.");
    return new ResponseEntity(...);
  }    
}

but see also this: Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]

csviri
  • 1,159
  • 3
  • 16
  • 31
0

I have encountered the same issue, after spending lot of time debugging the issue I found the jackson is not properly deserializing the ErrorResponse object.

This was because I didn't added the getters and setters for the field defined in the ErrorResponse object. I was initializing the fields using constructor and there were no getters and setters defined for those fields.

SOLUTION (for empty body):

So when I updated my ErrorResponse object from

import com.fasterxml.jackson.annotation.JsonRootName;
import java.util.List;

@JsonRootName("error")
public class ErrorResponse {

  private String message;
  private List<String> details;

  public ErrorResponse(String message, List<String> details) {
    this.message = message;
    this.details = details;
  }
}

to the following one with getters and setters

import com.fasterxml.jackson.annotation.JsonRootName;
import java.util.List;

@JsonRootName("error")
public class ErrorResponse {

  private String message;
  private List<String> details;

  public ErrorResponse(String message, List<String> details) {
    this.message = message;
    this.details = details;
  }

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public List<String> getDetails() {
    return details;
  }

  public void setDetails(List<String> details) {
    this.details = details;
  }
}

Jackson is now deserializing the ErrorResponse properly and I'm getting the serialized body in the response.

Vivek
  • 11,938
  • 19
  • 92
  • 127