I have defined a global exception handling in my Spring Boot based Rest service:
#ControllerAdvice
public class GlobalExceptionController {
private final Logger LOG = LoggerFactory.getLogger(getClass());
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "Internal application error")
#ExceptionHandler({ServiceException.class})
#ResponseBody
public ServiceException serviceError(ServiceException e) {
LOG.error("{}: {}", e.getErrorCode(), e.getMessage());
return e;
}
}
and a custom ServiceException:
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = -6502596312985405760L;
private String errorCode;
public ServiceException(String message, String errorCode, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
// other constructors, getter and setters omitted
}
so far so good, when an exception is fired the controller works as it should and respond with:
{
"timestamp": 1413883870237,
"status": 500,
"error": "Internal Server Error",
"exception": "org.example.ServiceException",
"message": "somthing goes wrong",
"path": "/index"
}
but the field errorCode isn't shown in the JSON response.
So how can I define a custom exception response in my application.
Spring Boot uses an implementation of ErrorAttributes to populate the Map that's rendered as JSON. By default, an instance of DefaultErrorAttributes is used. To include your custom errorCode you'll need to provide a custom ErrorAttributes implementation that knows about ServiceException and its error code. This custom implementation should be an a #Bean in your configuration.
One approach would be to sub-class DefaultErrorAttributes:
#Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
#Override
public Map<String, Object> getErrorAttributes(
RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(requestAttributes, includeStackTrace);
Throwable error = getError(requestAttributes);
if (error instanceof ServiceException) {
errorAttributes.put("errorCode", ((ServiceException)error).getErrorCode());
}
return errorAttributes;
}
};
}
#Alex You can use annotation #ExceptionHandler(YourExceptionClass.class) to handle the specific exception in specific RestController. I think it's a better way to handle complicated scenarios in business applications. Moreover i will suggest you to use custom exception translator to deal with different type of exceptions. You can consider spring oauth2 exception translator as reference code for exception translator.
Note: Following code is only to understand concept of this solution. It's not production ready code. Feel free to discuss more about it.
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
/**
* #author Harpreet
* #since 16-Aug-17.
*/
#RestController
public class RestTestController {
#RequestMapping(value = "name", method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseObject name(#RequestParam(value="name") String value){
//your custom logic
if (value == null || value.length() < 2) {
//throwing exception to invoke customExceptionHandler
throw new NullPointerException("value of request_param:name is invalid");
}
ResponseObject responseObject = new ResponseObject();
responseObject.setMessage(value)
.setErrorCode(1);
return responseObject;
}
// to handle null pointer exception
#ExceptionHandler(NullPointerException.class)
public ResponseObject customExceptionHandler
(NullPointerException e) {
ResponseObject responseObject = new ResponseObject();
responseObject.setMessage(e.getMessage())
.setErrorCode(-1);
return responseObject;
}
// response data transfer class
static class ResponseObject{
String message;
Integer errorCode;
public String getMessage() {
return message;
}
public ResponseObject setMessage(String message) {
this.message = message;
return this;
}
public Integer getErrorCode() {
return errorCode;
}
public ResponseObject setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
return this;
}
}
}
Related
everyone!
I making a defense against password brute force.
I successfully handle AuthenticationFailureBadCredentialsEvent when the user writes the right login and wrong password. But the problem is that I want to return JSON with two fields
{
message : '...' <- custom message
code : 'login_failed'
}
The problem is that it returns standart forbidden exception, but I need custom json.
#Log4j2
#Component
#RequiredArgsConstructor
public class AuthenticationAttemptsHandler {
protected final MessageSource messageSource;
private final AuthenticationAttemptsStore attemptsStore;
private final UserDetailsService userDetailsService;
private final UserDetailsLockService userDetailsLockService;
#EventListener
public void handleFailure(AuthenticationFailureBadCredentialsEvent event) {
val authentication = event.getAuthentication();
val userDetails = findUserDetails(authentication.getName());
userDetails.ifPresent(this::failAttempt);
}
private Optional<UserDetails> findUserDetails(String username) {
...
}
private void failAttempt(UserDetails details) {
val username = details.getUsername();
val attempt = attempt(loginAttemptsProperties.getResetFailuresInterval());
int failures = attemptsStore.incrementFailures(username, attempt);
if (failures >= 2) {
Instant lockedUntil = Instant.now().plus(loginAttemptsProperties.getLockDuration());
userDetailsLockService.lockUser(username, lockedUntil);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
String date = formatter.format(lockedUntil);
String message = String.format("Account will locked till %s", date);
throw new SecurityException(message);
//FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message, //
//"login_ failed"); <---- tryed return entity from this method. Does not work.
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
} else {
String message = String.format("You have %s attempts.", (3 - failures));
// FailAttemptsExceptionResponse response = new FailAttemptsExceptionResponse(message,
"login_ failed");
throw new SecurityException(message);
// return new ResponseEntity<>(response,HttpStatus.FORBIDDEN);
}
}
}
RuntimeException returns 500 status? but I need forbidden
public class SecurityException extends RuntimeException {
private static final long serialVersionUID = 1L;
public SecurityException(String msg) {
super(msg);
}
}
Responce model
public class FailAttemptsExceptionResponse {
String message;
String code;
public FailAttemptsExceptionResponse(String message, String code) {
super();
this.message = message;
this.code = code;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
}
Tried to handle SecurityException and then returns model? but it does not work
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
I successfully handle AuthenticationFailureBadCredentialsEvent, but how can I return JSON response model from the handler with a custom message?
#ControllerAdvice
public class SeurityAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(SecurityException.class)
public ResponseEntity<FailAttemptsExceptionResponse> handleNotFoundException(SecurityException ex, HttpServletResponse response) {
FailAttemptsExceptionResponse exceptionResponse = new FailAttemptsExceptionResponse(ex.getMessage(),
"login_ failed");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return new ResponseEntity<FailAttemptsExceptionResponse>(exceptionResponse,
HttpStatus.NOT_ACCEPTABLE);
}
}
maybe you need to add HttpServletResponse and set the http status.
Register the entry point
As mentioned, I do it with Java Config. I just show the relevant configuration here, there should be other configuration such as session stateless, etc.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().authenticationEntryPoint(new CustomEntryPoint());
}
}
U can create AuthenticationEntryPoint.
Короч тут почитай xD
Handle spring security authentication exceptions with #ExceptionHandler
Have a custom error controller on Spring boot:
package com.example.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.boot.web.servlet.error.ErrorController;
import javax.servlet.http.HttpServletRequest;
#Controller
public class CustomErrorController implements ErrorController
{
#RequestMapping("/error")
public String handleError(HttpServletRequest request)
{
...
}
#Override
public String getErrorPath()
{
return "/error";
}
}
But, when compile says: getErrorPath() in ErrorController has been deprecated. Ok, i found information: use server.error.path property. Ok, add this in application.properties and delete the function, but now says: CustomErrorController is not abstract and does not override abstract method getErrorPath() in ErrorController, ¿need a deprecated function?.
How to made the custom error controller?, the ErrorController requires getErrorPath but it is deprecated, what is the correct alternative?.
Starting version 2.3.x, Spring boot has deprecated this method. Just return null as it is anyway going to be ignored. Do not use #Override annotation if you want to prevent future compilation error when the method is totally removed. You can also suppress the deprecation warning if you want, however, the warning (also the #Override annotation) is helpful to remind you to cleanup/fix your code when the method is removed.
#Controller
#RequestMapping("/error")
#SuppressWarnings("deprecation")
public class CustomErrorController implements ErrorController {
public String error() {
// handle error
// ..
}
public String getErrorPath() {
return null;
}
}
#Controller
public class CustomErrorController implements ErrorController {
#RequestMapping("/error")
public ModelAndView handleError(HttpServletResponse response) {
int status = response.getStatus();
if ( status == HttpStatus.NOT_FOUND.value()) {
System.out.println("Error with code " + status + " Happened!");
return new ModelAndView("error-404");
} else if (status == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
System.out.println("Error with code " + status + " Happened!");
return new ModelAndView("error-500");
}
System.out.println(status);
return new ModelAndView("error");
}
}
there is an #ControllerAdvice annotation
#ControllerAdvice
public class MyErrorController {
#ExceptionHandler(RuntimeException.class)
public String|ResponseEntity|AnyOtherType handler(final RuntimeException e) {
.. do handle ..
}
#ExceptionHandler({ Exception1.class, Exception2.class })
public String multipleHandler(final Exception e) {
}
}
To handle errors, There is no need to define a controller class
implementing an error controller.
To handle errors in your entire application instead of writing
#Controller
public class CustomErrorController implements ErrorController{
#RequestMapping("/error")
public String handleError(HttpServletRequest request)
{
...
}
}
use the below class
#ControllerAdvice
public class myExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
public final ResponseEntity<YourResponseClass> handleAllExceptions(Exception ex, WebRequest request) {
YourResponseClassexceptionResponse = new YourResponseClass(new Date(), ex.getMessage());// Its an example you can define a class with your own structure
return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(CustomException.class)
public final ResponseEntity<YourResponseClass> handleAllExceptions(Exception ex, WebRequest request) {
YourResponseClass exceptionResponse = new YourResponseClass(new Date(), ex.getMessage()); // For reference
return new ResponseEntity<YourResponseClass>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
#ExceptionHandler(BadCredentialsException.class)
public final ResponseEntity<YourResponseClass> handleBadCredentialsException(BadCredentialsException ex, WebRequest request){
YourResponseClass exceptionResponse = new YourResponseClass(new Date(), ex.getMessage());// For refernece
return new ResponseEntity<>(exceptionResponse, HttpStatus.UNAUTHORIZED);
}
}
The class above annoted with #ControllerAdvice acts as custom exception handler and it handles all the expecptions thrown by ur application. In above code sample only three exceptions are showed for understanding. It can handle many exceptions
In your application if there's any exception thrown it will come to this class and send the response back. You can have a customized message and structure as per ur needs.
#Controller
public class AppErrorController implements ErrorController {
#RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if(status != null) {
int statusCode = Integer.valueOf(status.toString());
if (statusCode == HttpStatus.FORBIDDEN.value()) {
return "errorpages/error-403";
} else if (statusCode == HttpStatus.NOT_FOUND.value()) {
return "errorpages/error-404";
} else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "errorpages/error-500";
}
}
return "errorpages/error";
}
}
I want to re-throw my exception from my "Database" REST API to my "Backend" REST API but I lose the original exception's message.
This is what i get from my "Database" REST API via Postman:
{
"timestamp": "2020-03-18T15:19:14.273+0000",
"status": 400,
"error": "Bad Request",
"message": "I'm DatabaseException (0)",
"path": "/database/api/vehicle/test/0"
}
This part is ok.
This is what i get from my "Backend" REST API via Postman:
{
"timestamp": "2020-03-18T15:22:12.801+0000",
"status": 400,
"error": "Bad Request",
"message": "400 BAD_REQUEST \"\"; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8090/database/api/vehicle/test/0",
"path": "/backend/api/vehicle/test/0"
}
As you can see the original "message" field is lost.
I use:
Spring boot 2.2.5.RELEASE
spring-boot-starter-web
spring-boot-starter-webflux
Backend and Database start with Tomcat (web and webflux in the same application).
This is Backend:
#GetMapping(path = "/test/{id}")
public Mono<Integer> test(#PathVariable String id) {
return vehicleService.test(id);
}
With vehicleService.test:
public Mono<Integer> test(String id) {
return WebClient
.create("http://localhost:8090/database/api")
.get()
.uri("/vehicle/test/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Integer.class);
}
This is Database:
#GetMapping(path = "/test/{id}")
public Mono<Integer> test(#PathVariable String id) throws Exception {
if (id.equals("0")) {
throw new DatabaseException("I'm DatabaseException (0)");
}
return Mono.just(Integer.valueOf(2));
}
I also tried with return Mono.error(new DatabaseException("I'm DatabaseException (0)"));
And DatabaseException:
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
It seems my Backend transforms the response and can't find any answer on internet.
Instead of retrieve of WebClient, you could use exchange which lets you handle the error and propagate a custom exception with a message retrieved from the service response.
private void execute()
{
WebClient webClient = WebClient.create();
webClient.get()
.uri("http://localhost:8089")
.exchangeToMono(this::handleResponse)
.doOnNext(System.out::println)
.block(); // not required, just for testing purposes
}
private Mono<Response> handleResponse(ClientResponse clientResponse)
{
if (clientResponse.statusCode().isError())
{
return clientResponse.bodyToMono(Response.class)
.flatMap(response -> Mono.error(new RuntimeException(response.message)));
}
return clientResponse.bodyToMono(Response.class);
}
private static class Response
{
private String message;
public Response()
{
}
public String getMessage()
{
return message;
}
public void setMessage(String message)
{
this.message = message;
}
#Override
public String toString()
{
return "Response{" +
"message='" + message + '\'' +
'}';
}
}
Code below is now working, it's another code than my original question but it's pretty much the same idea (with Backend REST api and Database REST api).
My Database REST api:
#RestController
#RequestMapping("/user")
public class UserControl {
#Autowired
UserRepo userRepo;
#Autowired
UserMapper userMapper;
#GetMapping("/{login}")
public Mono<UserDTO> getUser(#PathVariable String login) throws DatabaseException {
User user = userRepo.findByLogin(login);
if(user == null) {
throw new DatabaseException(HttpStatus.BAD_REQUEST, "error.user.not.found");
}
return Mono.just(userMapper.toDTO(user));
}
}
UserRepo is just a #RestReporitory.
UserMapper use MapStruct to map my Entity to DTO object.
With :
#Data
#EqualsAndHashCode(callSuper=false)
public class DatabaseException extends ResponseStatusException {
private static final long serialVersionUID = 1L;
public DatabaseException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
#Data & EqualsAndHashCode come from Lombok library.
Extends ResponseStatusException is very important here, if you don't do that then response will be bad handled.
My Backend REST api which receives data from Database REST API:
#RestController
#RequestMapping("/user")
public class UserControl {
#Value("${database.api.url}")
public String databaseApiUrl;
private String prefixedURI = "/user";
#GetMapping("/{login}")
public Mono<UserDTO> getUser(#PathVariable String login) {
return WebClient
.create(databaseApiUrl)
.get()
.uri(prefixedURI + "/{login}", login).retrieve()
.onStatus(HttpStatus::isError, GlobalErrorHandler::manageError)
.bodyToMono(UserDTO.class);
}
}
With GlobalErrorHandler::
public class GlobalErrorHandler {
/**
* Translation key for i18n
*/
public final static String I18N_KEY_ERROR_TECHNICAL_EXCEPTION = "error.technical.exception";
public static Mono<ResponseStatusException> manageError(ClientResponse clientResponse) {
if (clientResponse.statusCode().is4xxClientError()) {
// re-throw original status and message or they will be lost
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(response.getStatus(), response.getMessage()));
});
} else { // Case when it's 5xx ClientError
// User doesn't have to know which technical exception has happened
return clientResponse.bodyToMono(ExceptionResponseDTO.class).flatMap(response -> {
return Mono.error(new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
I18N_KEY_ERROR_TECHNICAL_EXCEPTION));
});
}
}
}
And ExceptionResponseDTO which is mandatory to retrieve some data from clientResponse:
/**
* Used to map ClientResponse from WebFlux
*/
#Data
#EqualsAndHashCode(callSuper=false)
public class ExceptionResponseDTO extends Exception {
private static final long serialVersionUID = 1L;
private HttpStatus status;
public ExceptionResponseDTO(String message) {
super(message);
}
/**
* Status has to be converted into {#link HttpStatus}
*/
public void setStatus(String status) {
this.status = HttpStatus.valueOf(Integer.valueOf(status));
}
}
One other related class which could be useful: ExchangeFilterFunctions.java
I found a lot of information in this issue:
https://github.com/spring-projects/spring-framework/issues/20280
Even if these information are old they are still relevent !
In my Spring application, I have a catch-all exception handler in my rest controller:
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public String otherExceptions(Exception e) {
return "Internal Server Error";
}
However, some exceptions are annotated with a specific http status code:
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class UserNotAuthenticatedException extends RuntimeException {
}
Unfortunately, the annotated exceptions are also handled by the exception handler in the controller. Is it possible to somehow tell Spring to handle them by returning the status code they are annotated with?
You're mixing two ways to handle an exception, but there's a work around for this.
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public String otherExceptions(Exception e) throws Exception {
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
return "Internal Server Error";
}
In this case, if an exception is annotated with ResponseStatus you rethrow the exception and let the framework handle it.
#ExceptionHandler(UserNotAuthenticatedException.class)
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public String otherExceptions(Exception e) {
return "Internal Server Error";
}
Create #ExceptionHandler for each separate exception class you need.
Below one solution that u can try
#ControllerAdvice
public class SampleExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler(Exception.class)
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public String otherExceptions(Exception e) {
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)
throw e;
return "Internal Server Error";
}
}
You can use #ControllerAdvice to handle custom exceptions for example for the exception UserNotAuthenticatedException :
1) Create a generic exception
public abstract class GenericException extends RuntimeException {
public GenericException (String messageKey) {
super(messageKey);
}
public GenericException (String messageKey, Throwable cause) {
super(messageKey, cause);
}
private static final long serialVersionUID = 1L;
public abstract HttpStatus getHttpStatus();
}
2) Create a specific exception with your annotation :
#ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotAuthenticatedException extends GenericException {
private static final long serialVersionUID = 1L;
public UserNotAuthenticatedException () {
super(ExceptionKeys.RESOURCE_NOT_FOUND_EXCEPTION);
}
public UserNotAuthenticatedException (Throwable cause) {
super(ExceptionKeys.RESOURCE_NOT_FOUND_EXCEPTION, cause);
}
public UserNotAuthenticatedException (String messageKey) {
super(messageKey);
}
public UserNotAuthenticatedException (String messageKey, Throwable cause) {
super(messageKey, cause);
}
#Override
public HttpStatus getHttpStatus() {
return HttpStatus.NOT_FOUND;
}
}
3) Create an error DTO :
#Data
public class ErrorDto {
private int code;
private String message;
private String additionalData;
public ErrorDto(String message, int code) {
super();
this.message = message;
this.code = code;
}
public ErrorDto(String message, String additionalData) {
super();
this.message = message;
this.additionalData = additionalData;
}
public ErrorDto() {
}
}
4) Then create a global exception handler to map your exception with status :
#ControllerAdvice
#RestController
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler({GenericException .class})
public ResponseEntity<ErrorDto> handlePortalExceptions(final GenericException pex) {
log.error("{} [{}]", pex.getClass().getName(), pex.getMessage());
log.error(pex.getMessage(), pex);
final ErrorDto errorDetails = new ErrorDto(pex.getMessage(), pex.getHttpStatus().value());
errorDetails.setAdditionalData(pex.getCause() != null ? pex.getCause().getLocalizedMessage() : pex.getLocalizedMessage());
return new ResponseEntity<>(errorDetails, pex.getHttpStatus());
}
#ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleAllExceptions(final Exception ex, WebRequest request) {
log.error("{} detected [{}]", ex.getClass().getName(), ex.getMessage());
log.error(ex.getMessage(), ex);
final ErrorDto errorDetails = new ErrorDto(ExceptionKeys.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.value());
errorDetails.setAdditionalData(ex.getCause() != null ? ex.getCause().getLocalizedMessage() : ex.getLocalizedMessage());
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
given the following dto and controller
public class PasswordCredentials implements AuthenticationProvider {
#NotNull
#NotEmpty
#JsonProperty( access = JsonProperty.Access.WRITE_ONLY )
private String user;
#NotNull
#NotEmpty
#JsonProperty( access = JsonProperty.Access.WRITE_ONLY )
private CharSequence pass;
public void setPass( final CharSequence pass ) {
this.pass = pass;
}
public void setUser( final String user ) {
this.user = user;
}
#Override
public Authentication toAuthentication() {
return new UsernamePasswordAuthenticationToken( user, pass );
}
}
#RestController
#RequestMapping( path = "authentication" )
class AuthenticationController {
private final AuthenticationManager am;
AuthenticationController( final AuthenticationManager am ) {
this.am = am;
}
#RequestMapping( path = "password", method = RequestMethod.POST, consumes = {
"!" + MediaType.APPLICATION_FORM_URLENCODED_VALUE
} )
ResponseEntity<?> login( #Valid #RequestBody final PasswordCredentials credentials ) {
Authentication authenticate = am.authenticate( credentials.toAuthentication() );
if ( authenticate.isAuthenticated() ) {
return ResponseEntity.status( HttpStatus.NO_CONTENT ).build();
}
return ResponseEntity.status( HttpStatus.FORBIDDEN ).build();
}
}
if for example pass is null there will be a validation error, and a 400 will happen without ever calling my controller, which is fine. That 400 however has no content, is there any way to have the controllers BindResults output as content so that the consumer of the API knows what caused the problem? Ideally I would not do this in the controller method, so that it would happen on all controllers?
I was able to get this behavior with spring data rest as follows, but I'd like it for all API controllers.
class RestConfig extends RepositoryRestConfigurerAdapter {
#Bean
Validator validator() {
return new LocalValidatorFactoryBean();
}
#Override
public void configureValidatingRepositoryEventListener(
final ValidatingRepositoryEventListener validatingListener ) {
Validator validator = validator();
//bean validation always before save and create
validatingListener.addValidator( "beforeCreate", validator );
validatingListener.addValidator( "beforeSave", validator );
}
#Override
public void configureRepositoryRestConfiguration( final RepositoryRestConfiguration config ) {
config.setBasePath( "/v0" );
config.setReturnBodyOnCreate( false );
config.setReturnBodyOnUpdate( false );
}
Spring have #ControllerAdvice and #ExceptionHandler annotation to handle errors in controllers.
#ControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public Error processValidationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
.....
return new Error();
}
// Other exceptions
}
i want to improve the answer of Anton Novopashin: just return the error in response entity.
#ControllerAdvice
public class ExceptionTranslator {
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseBody
public ResponseEntity<String> processValidationError(MethodArgumentNotValidException ex) {
return new ResponseEntity(ex.getMessage, HttpStatus.BAD_REQUEST);
}
// Other exceptions
}
I'm not sure who or why downvoted the existing answers but they are both right - the best way to handle validation errors would be to declare a #ControllerAdvice and then handle the exceptions there. Here's a snippet of my global error handler, taken from an existing project:
#ControllerAdvice
#ResponseBody
public class RestfulErrorHandler {
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse methodValidationError(MethodArgumentNotValidException e) {
final ErrorResponse response = new ErrorResponse();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
if (error instanceof FieldError) {
response.addFieldError((FieldError) error);
} else {
response.addGeneralError(error.getDefaultMessage());
}
}
return response;
}
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ConstraintViolationException.class)
public ErrorResponse constraintViolationError(ConstraintViolationException e) {
final ErrorResponse response = new ErrorResponse();
for (ConstraintViolation<?> v : e.getConstraintViolations()) {
response.addFieldError(v.getPropertyPath(), v.getMessage());
}
return response;
}
}
You should also process ConstraintViolationExceptions since they too may be thrown. Here's a link to my ErrorResponse class, I'm including it as a Gist so as not to obscure the main point.
You should also probably process the RepositoryConstraintViolationException, I'm not sure if spring-data-rest handles them already.