When a MethodArgumentNotValidException is thrown in my Spring app I would like the response to:
Have a 400 code
Have a body similar to the one you get when you throw a ResponseStatusException in a regular controller
Here's my code for now:
#ControllerAdvice
public class CustomExceptionHandlers extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
FieldError err = ex.getBindingResult().getFieldErrors().get(0);
String msg = "Unknown error";
if (err != null) {
msg = err.getField() + ": " + err.getDefaultMessage();
}
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg);
}
}
The problem is: it does nothing (I guess). I feel like I shouldn't throw an exception in a ResponseEntityExceptionHandler.
Any idea?
Edit: Apparently I want what DefaultErrorAttributes produces, but I still don't know how to use it.
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class CustomExceptionHandler {
#ResponseStatus(BAD_REQUEST)
#ResponseBody
#ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> methodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) {
// Make the message
FieldError err = ex.getBindingResult().getFieldErrors().get(0);
String msg = "Unknown error";
if (err != null) {
msg = err.getField() + ": " + err.getDefaultMessage();
}
// Set attributes
request.setAttribute("javax.servlet.error.status_code", BAD_REQUEST.value(), WebRequest.SCOPE_REQUEST);
request.setAttribute("javax.servlet.error.message", msg, WebRequest.SCOPE_REQUEST);
// Return the error
DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
return ResponseEntity.badRequest().body(errorAttributes.getErrorAttributes(request, false));
}
}
I'm trying to override the handleMethodArgumentNotValid method but without success.
The error I'm getting is: "the method handleMethodArgumentNotValid(MethodArgumentNotValidException, HttpHeaders, HttpStatus, WebRequest) of type CustomResponseEntityExceptionHandler must override or implement a supertype but I don't know why I'm not doing it already.
Here's what I'm trying:
#ControllerAdvice
#RestController
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
var exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<Object>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return new ResponseEntity<Object>("test", HttpStatus.NOT_ACCEPTABLE);
}
#ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundExceptions(Exception ex, WebRequest request) {
var exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<Object>(exceptionResponse, HttpStatus.NOT_FOUND);
}
}
I'm following this tutorial if it's any help: https://www.baeldung.com/global-error-handler-in-a-spring-rest-api
Well if you are not using other parameters try this
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
return new ResponseEntity<Object>("test", HttpStatus.NOT_ACCEPTABLE);
}
I have requirements that if the JSON for the post request is invalid, I will need to send 400 HTTP response codes and if any fields are not parsable, the return status code will be 422. An example post request can be:
{
"amount": "12.3343",
"timestamp": "2018-07-17T09:59:51.312Z"
}
The Dto class is provided as below,
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class TransactionDto {
#NotNull
#Min(0)
private BigDecimal amount;
#NotNull
private LocalDateTime timestamp;
}
This is the controller with POST request,
#Slf4j
#RestController
#RequestMapping("/")
#Validated
public class TransactionController {
#Autowired
private TransactionService transactionService;
#Operation(description = "create a transaction using the provided JSON data")
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Create transaction", content = {
#Content(mediaType = "application/json", schema = #Schema(implementation = Transaction.class))}),
#ApiResponse(responseCode = "204", description = "Transaction is older than 60 seconds", content = #Content(mediaType = "application/json")),
#ApiResponse(responseCode = "500", description = MessageConstant.INTERNAL_SERVER_ERROR_MSG, content = #Content)})
#PostMapping(value = "/transactions")
public ResponseEntity<Object> createProperty(#RequestBody #Valid TransactionDto transactionDto) {
try {
final LocalDateTime transactionTimestamp = transactionDto.getTimestamp();
final LocalDateTime localDateTimeNow = LocalDateTime.now(ZoneOffset.UTC);
final long secondsDuration = Duration.between(transactionTimestamp, localDateTimeNow).toSeconds();
final boolean isFuture = transactionTimestamp.isAfter(localDateTimeNow);
if (isFuture) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.UNPROCESSABLE_ENTITY,
"Transaction is in future"), new HttpHeaders(), HttpStatus.ACCEPTED);
}
if (secondsDuration > 60) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.NO_CONTENT,
"Transaction is older than 60 seconds"), new HttpHeaders(), HttpStatus.NO_CONTENT);
}
final Transaction transaction = transactionService.createTransaction(transactionDto);
return new ResponseEntity<>(transaction, new HttpHeaders(), HttpStatus.CREATED);
} catch (Exception ex) {
log.error(MessageConstant.INTERNAL_SERVER_ERROR_MSG + ex.getMessage());
return new ResponseEntity<>(ApiResponseMessage.getInternalServerError(), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
If the "amount" is, say, "sfdfd", this is not BigDecimal, we should provide the 422. But if the "amount" is "-12.3343", this is a constraint validation error but the data is valid and parsable. So we can't have the 422.
This is my exception handling class,
#Slf4j
#ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#Override
#Nonnull
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
String error = ex.getParameterName() + " parameter is missing";
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.BAD_REQUEST,
error), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
#Override
#Nonnull
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
#Nonnull HttpMediaTypeNotSupportedException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
String message = prepareMessageFromException(ex, (ServletWebRequest) request);
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.UNSUPPORTED_MEDIA_TYPE,
" media type is not supported. Supported media types "), new HttpHeaders(), HttpStatus.UNSUPPORTED_MEDIA_TYPE);
}
#Override
#NonNull
protected ResponseEntity<Object> handleMethodArgumentNotValid(
#NonNull MethodArgumentNotValidException ex,
#NonNull HttpHeaders headers,
#NonNull HttpStatus status,
#NonNull WebRequest request
) {
String message = prepareMessageFromException(ex, (ServletWebRequest) request);
ApiErrorResponse apiError = new ApiErrorResponse(BAD_REQUEST);
apiError.setMessage("Validation error");
apiError.addValidationErrors(ex.getBindingResult().getFieldErrors());
apiError.addValidationError(ex.getBindingResult().getGlobalErrors());
return buildResponseEntity(apiError);
}
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex) {
ApiErrorResponse apiError = new ApiErrorResponse(BAD_REQUEST);
apiError.setMessage("Validation error");
apiError.addValidationErrors(ex.getConstraintViolations());
return buildResponseEntity(apiError);
}
#ExceptionHandler(Exception.class)
#ResponseBody
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
ServletWebRequest req = (ServletWebRequest) request;
String message = prepareMessageFromException(ex, (ServletWebRequest) request);
log.info("{} to {}", req.getHttpMethod(), req.getRequest().getServletPath());
log.error(message, ex);
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.BAD_REQUEST,
message), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
#Override
#Nonnull
protected ResponseEntity<Object> handleHttpMessageNotReadable(
#Nonnull HttpMessageNotReadableException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
String message = prepareMessageFromException(ex, (ServletWebRequest) request);
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, BAD_REQUEST,
message), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
private String prepareMessageFromException(Exception ex, ServletWebRequest request) {
String message = ex.getMessage();
log.info("{} to {}", request.getHttpMethod(), request.getRequest().getServletPath());
log.error(message);
if (message != null && !message.isEmpty()) {
message = message.split(":")[0];
}
return message;
}
#Override
#Nonnull
protected ResponseEntity<Object> handleHttpMessageNotWritable(
#Nonnull HttpMessageNotWritableException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
String error = "Error writing JSON output";
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR,
"Internal server error. please contact support !!"), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
#Override
#Nonnull
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.BAD_REQUEST,
String.format("Could not find the %s method for URL %s", ex.getHttpMethod(), ex.getRequestURL())), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
private ResponseEntity<Object> buildResponseEntity(ApiErrorResponse apiError) {
return new ResponseEntity<>(apiError, apiError.getStatus());
}
#ExceptionHandler(EntityNotFoundException.class)
protected ResponseEntity<Object> handleEntityNotFound(EntityNotFoundException ex) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.NOT_FOUND,
"Resource not found: "), new HttpHeaders(), HttpStatus.NOT_FOUND);
}
#ExceptionHandler(DataIntegrityViolationException.class)
protected ResponseEntity<Object> handleDataIntegrityViolation(DataIntegrityViolationException ex, WebRequest request) {
if (ex.getCause() instanceof ConstraintViolationException) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.CONFLICT,
"Database error"), new HttpHeaders(), HttpStatus.CONFLICT);
}
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.INTERNAL_SERVER_ERROR,
"Internal server error. please contact support !!" + ex.getMessage()), new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex, WebRequest request) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.BAD_REQUEST,
String.format("The parameter '%s' of value '%s' could not be converted to type '%s'", ex.getName(), ex.getValue(), ex.getRequiredType())), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
}
At the moment, I get 400 in both of the cases mentioned. How do I refactor the code to get the correct response code?
I manage to achieve that after some modification of the existing code,
#Override
#Nonnull
protected ResponseEntity<Object> handleHttpMessageNotReadable(
#Nonnull HttpMessageNotReadableException ex,
#Nonnull HttpHeaders headers,
#Nonnull HttpStatus status,
#Nonnull WebRequest request
) {
String message = prepareMessageFromException(ex, (ServletWebRequest) request);
final Throwable throwableCause = ex.getCause();
if (throwableCause instanceof InvalidFormatException) {
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.UNPROCESSABLE_ENTITY,
message), new HttpHeaders(), HttpStatus.UNPROCESSABLE_ENTITY);
}
return new ResponseEntity<>(ApiResponseMessage.getGenericApiResponse(Boolean.FALSE, HttpStatus.BAD_REQUEST,
message), new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
So when the throwable is InvalidFormatException, I will return the 422 response status code and other the method works as before.
I can't seem to be able to make the following code work.
For some reason neither of the following advice is being thrown to the controller.
#ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
#Autowired
private MessageSource messageSource;
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String mensagemUsuario = messageSource.getMessage("mensagem.invalida", null, LocaleContextHolder.getLocale());
String mensagemDesenvolvedor = ex.getCause().toString();
List<Erro> erros = Arrays.asList(new Erro(mensagemUsuario, mensagemDesenvolvedor));
return handleExceptionInternal(ex, erros, headers, HttpStatus.BAD_REQUEST, request);
}
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<Erro> erros = criarListaDeErros(ex.getBindingResult());
return handleExceptionInternal(ex, erros, headers, HttpStatus.BAD_REQUEST, request);
}
private List<Erro> criarListaDeErros(BindingResult bindingResult) {
List<Erro> erros = new ArrayList<>();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
String mensagemUsuario = messageSource.getMessage(fieldError, LocaleContextHolder.getLocale());
String mensagemDesenvolvedor = fieldError.toString();
erros.add(new Erro(mensagemUsuario, mensagemDesenvolvedor));
}
return erros;
}
#org.springframework.web.bind.annotation.ExceptionHandler({EmptyResultDataAccessException.class})
#ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<Object> handleEmptyResultDataAccessException(RuntimeException ex, WebRequest request){
String mensagemUsuario = messageSource.getMessage("mensagem.invalida", null, LocaleContextHolder.getLocale());
String mensagemDesenvolvedor = ex.getCause().toString();
List<Erro> erros = Arrays.asList(new Erro(mensagemUsuario, mensagemDesenvolvedor));
return handleExceptionInternal(ex, erros, new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
#org.springframework.web.bind.annotation.ExceptionHandler({DataIntegrityViolationException.class})
public ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest webRequest) {
String mensagemUsuario = messageSource.getMessage("mensagem.invalida", null, LocaleContextHolder.getLocale());
String mensagemDesenvolvedor = ex.getCause().toString();
List<Erro> erros = Arrays.asList(new Erro(mensagemUsuario, mensagemDesenvolvedor));
return handleExceptionInternal(ex, erros, new HttpHeaders(), HttpStatus.BAD_REQUEST, webRequest);
}
public static class Erro {
private String mensagemUsuario;
private String mensagemDesenvolvedor;
public Erro(String mensagemUsuario, String mensagemDesenvolvedor) {
this.mensagemUsuario = mensagemUsuario;
this.mensagemDesenvolvedor = mensagemDesenvolvedor;
}
} }
Project Structure :
It's my first time creating controller advice this way so would very much appreciate if someone could point me what I'm doing wrong.
Thank you.
I have added customexception class to handle my api class.
Exception class
#SuppressWarnings({ "unchecked", "rawtypes" })
#ControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorMessage error = new ErrorMessage("Server Error", details);
return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(RecordNotFoundException.class)
public final ResponseEntity<Object> handleConfigNotFoundException(RecordNotFoundException ex, WebRequest request) {
List<String> details = new ArrayList<>();
details.add(ex.getLocalizedMessage());
ErrorMessage error = new ErrorMessage("Record Not Found", details);
return new ResponseEntity(error, HttpStatus.NOT_FOUND);
}
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> details = new ArrayList<>();
for (ObjectError error : ex.getBindingResult().getAllErrors()) {
details.add(error.getDefaultMessage());
}
ErrorMessage error = new ErrorMessage("Validation Failed", details);
return new ResponseEntity(error, HttpStatus.BAD_REQUEST);
}
}
API method
#RequestMapping(value = "/config", params = { "appCode", "appVersion" }, method = RequestMethod.GET)
public ResponseEntity<ConfigResponse> getConfig(#RequestParam(value = "appCode", required = true) String appCode,
#RequestParam(value = "appVersion", required = true) String appVersion) {
List<AppConfig> result = configRepository.findByCodeAndVersion(appCode, appVersion);
if (result.isEmpty()) {
throw new RecordNotFoundException(appCode + " or " + appVersion + "does not exist");
}
AppConfig config = new AppConfig();
return new ResponseEntity<ConfigResponse>(config.convertToResponse(result), HttpStatus.OK);
}
Record Not found
#ResponseStatus(HttpStatus.NOT_FOUND)
public class RecordNotFoundException extends RuntimeException {
public RecordNotFoundException(String exception) {
super(exception);
}
}
I am checking for the record not found but it still gives me the spring exception and not my custom exception.