Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
wimdeblauwe committed Sep 25, 2024
2 parents 72692e5 + c053c44 commit ebcd322
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 21 deletions.
5 changes: 5 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ NOTE: Documentation is very important to us, so if you find something missing fr
|===
|error-handling-spring-boot-starter |Spring Boot|Minimum Java version|Docs

|https://github.com/wimdeblauwe/error-handling-spring-boot-starter/releases/tag/4.5.0[4.5.0]
|3.3.x
|17
|https://wimdeblauwe.github.io/error-handling-spring-boot-starter/4.5.0/[Documentation 4.5.0]

|https://github.com/wimdeblauwe/error-handling-spring-boot-starter/releases/tag/4.4.0[4.4.0]
|3.3.x
|17
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</parent>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>error-handling-spring-boot-starter</artifactId>
<version>4.4.0</version>
<version>4.5.0</version>
<name>Error Handling Spring Boot Starter</name>
<description>Spring Boot starter that configures error handling</description>

Expand Down
69 changes: 61 additions & 8 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,58 @@ The response JSON:

==== General override of error messages

By using `error.handling.messages` property, it is possible to globally set an error message for a certain exception.
This is most useful for the validation messages.
By using `error.handling.messages` property, it is possible to globally set an error message for a certain exception or a certain validation annotation.

===== Exception

Suppose you have this defined:

[source,properties]
----
error.handling.messages.com.company.application.user.UserNotFoundException=The user was not found
----

The response JSON:

[source,json]
----
{
"code": "USER_NOT_FOUND",
"message": "The user was not found" //<.>
}
----

<.> The output uses the configured override.

This can also be used for exception types that are not part of your own application.

For example:

[source,properties]
----
error.handling.messages.jakarta.validation.ConstraintViolationException=There was a validation failure.
----

Will output the following JSON:

[source,json]
----
{
"code": "VALIDATION_FAILED",
"message": "There was a validation failure.",
"fieldErrors": [
{
"code": "INVALID_SIZE",
"property": "name",
"message": "size must be between 10 and 2147483647",
"rejectedValue": "",
"path": "name"
}
]
}
----

===== Validation annotation

Suppose you have this defined:

Expand Down Expand Up @@ -1212,8 +1262,10 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageM
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
public class WebSecurityConfiguration {
@Bean
Expand All @@ -1227,11 +1279,11 @@ public class WebSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
UnauthorizedEntryPoint unauthorizedEntryPoint) throws Exception {
http.httpBasic().disable();
http.httpBasic(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests().anyRequest().authenticated();
http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated());
http.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint);//<.>
http.exceptionHandling(customizer -> customizer.authenticationEntryPoint(unauthorizedEntryPoint));//<.>
return http.build();
}
Expand All @@ -1258,6 +1310,7 @@ import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMap
import org.springframework.context.annotation.Bean;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
public class WebSecurityConfiguration {
Expand All @@ -1272,11 +1325,11 @@ public class WebSecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
AccessDeniedHandler accessDeniedHandler) throws Exception {
http.httpBasic().disable();
http.httpBasic(AbstractHttpConfigurer::disable);
http.authorizeHttpRequests().anyRequest().authenticated();
http.authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated());
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);//<.>
http.exceptionHandling(customizer -> customizer.accessDeniedHandler(accessDeniedHandler));//<.>
return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter;

import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.BindApiExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.HandlerMethodValidationExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.HttpMessageNotReadableApiExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.handler.TypeMismatchApiExceptionHandler;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
Expand Down Expand Up @@ -91,6 +92,14 @@ public BindApiExceptionHandler bindApiExceptionHandler(ErrorHandlingProperties p
return new BindApiExceptionHandler(properties, httpStatusMapper, errorCodeMapper, errorMessageMapper);
}

@Bean
@ConditionalOnMissingBean
public HandlerMethodValidationExceptionHandler handlerMethodValidationExceptionHandler(HttpStatusMapper httpStatusMapper,
ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper) {
return new HandlerMethodValidationExceptionHandler(httpStatusMapper, errorCodeMapper, errorMessageMapper);
}

@Bean
@ConditionalOnMissingBean
public ApiErrorResponseSerializer apiErrorResponseSerializer(ErrorHandlingProperties properties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ElementKind;
import jakarta.validation.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;

import java.util.Comparator;
import java.util.Optional;
Expand Down Expand Up @@ -136,6 +135,7 @@ private String getMessage(ConstraintViolation<?> constraintViolation) {
}

private String getMessage(ConstraintViolationException exception) {
return "Validation failed. Error count: " + exception.getConstraintViolations().size();
return errorMessageMapper.getErrorMessageIfConfiguredInProperties(exception)
.orElseGet(() -> "Validation failed. Error count: " + exception.getConstraintViolations().size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.wimdeblauwe.errorhandlingspringbootstarter.handler;

import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiErrorResponse;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiFieldError;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.ApiGlobalError;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorCodeMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.ErrorMessageMapper;
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpStatusMapper;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.method.annotation.HandlerMethodValidationException;

import java.util.List;
import java.util.Optional;

public class HandlerMethodValidationExceptionHandler extends AbstractApiExceptionHandler {

public HandlerMethodValidationExceptionHandler(HttpStatusMapper httpStatusMapper,
ErrorCodeMapper errorCodeMapper,
ErrorMessageMapper errorMessageMapper) {

super(httpStatusMapper, errorCodeMapper, errorMessageMapper);
}

@Override
public boolean canHandle(Throwable exception) {
return exception instanceof HandlerMethodValidationException;
}

@Override
public ApiErrorResponse handle(Throwable ex) {
var response = new ApiErrorResponse(HttpStatus.BAD_REQUEST, getErrorCode(ex), getErrorMessage(ex));
var validationException = (HandlerMethodValidationException) ex;
List<? extends MessageSourceResolvable> errors = validationException.getAllErrors();

errors.forEach(error -> {
if (error instanceof FieldError fieldError) {
var apiFieldError = new ApiFieldError(
errorCodeMapper.getErrorCode(fieldError.getCode()),
fieldError.getField(),
errorMessageMapper.getErrorMessage(fieldError.getCode(), fieldError.getDefaultMessage()),
fieldError.getRejectedValue(),
null);
response.addFieldError(apiFieldError);
} else {
var lastCode = Optional.ofNullable(error.getCodes())
.filter(codes -> codes.length > 0)
.map(codes -> codes[codes.length - 1])
.orElse(null);
var apiGlobalErrorMessage = new ApiGlobalError(
errorCodeMapper.getErrorCode(lastCode),
errorMessageMapper.getErrorMessage(lastCode, error.getDefaultMessage()));
response.addGlobalError(apiGlobalErrorMessage);
}
});

return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.github.wimdeblauwe.errorhandlingspringbootstarter.ErrorHandlingProperties;

import java.util.Optional;

import static org.springframework.util.StringUtils.hasText;

/**
Expand All @@ -22,6 +24,10 @@ public String getErrorMessage(Throwable exception) {
return exception.getMessage();
}

public Optional<String> getErrorMessageIfConfiguredInProperties(Throwable exception) {
return Optional.ofNullable(getErrorMessageFromProperties(exception.getClass()));
}

public String getErrorMessage(String fieldSpecificCode, String code, String defaultMessage) {
if (properties.getMessages().containsKey(fieldSpecificCode)) {
return properties.getMessages().get(fieldSpecificCode);
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/error-handling-defaults.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
error.handling.codes.org.springframework.web.bind.MethodArgumentNotValidException=VALIDATION_FAILED
error.handling.codes.org.springframework.web.method.annotation.HandlerMethodValidationException=VALIDATION_FAILED
error.handling.messages.org.springframework.web.method.annotation.HandlerMethodValidationException=There was a validation failure.
error.handling.codes.org.springframework.http.converter.HttpMessageNotReadableException=MESSAGE_NOT_READABLE
error.handling.codes.jakarta.validation.ConstraintViolationException=VALIDATION_FAILED
error.handling.codes.org.springframework.beans.TypeMismatchException=TYPE_MISMATCH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ void testErrorCodeOverride(@Autowired ErrorHandlingProperties properties) throws
;
}

@Test
@WithMockUser
void testErrorMessageOverride(@Autowired ErrorHandlingProperties properties) throws Exception {
properties.getMessages().put("jakarta.validation.ConstraintViolationException", "There was a validation failure.");
mockMvc.perform(post("/test/validation")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"value2\": \"\"}")
.with(csrf()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("code").value("VALIDATION_FAILED"))
.andExpect(jsonPath("message").value("There was a validation failure."))
;
}

@Test
@WithMockUser
void testFieldErrorCodeOverride(@Autowired ErrorHandlingProperties properties) throws Exception {
Expand Down
Loading

0 comments on commit ebcd322

Please sign in to comment.