Wrap 'error' attribute for consistent JSON serialization

Update `DefaultErrorAttributes` implementations so that errors
are wrapped for consistent JSON serialization.

Prior to this commit, only `ObjectError` implementations were
included in the 'errors' entry.

Signed-off-by: yongjunhong <kevin0928@naver.com>

See gh-43330
This commit is contained in:
yongjunhong
2024-11-30 00:49:23 +09:00
committed by Phillip Webb
parent 1c991a7e5e
commit 13f00f7a8a
4 changed files with 136 additions and 18 deletions

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.error;
import jakarta.annotation.Nullable;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.util.Assert;
/**
* A wrapper class for error objects that implements {@link MessageSourceResolvable}.
* This class extends {@link DefaultMessageSourceResolvable} and delegates the
* message resolution to the wrapped error object.
*
* @author Yongjun Hong
* @since 3.5.0
*/
public class ErrorWrapper extends DefaultMessageSourceResolvable {
private final Object error;
/**
* Create a new {@code ErrorWrapper} instance with the specified error.
*
* @param error the error object to wrap (must not be {@code null})
*/
public ErrorWrapper(Object error) {
this(error, null, null, null);
}
/**
* Create a new {@code ErrorWrapper} instance with the specified error, codes,
* arguments, and default message.
*
* @param error the error object to wrap (must not be {@code null})
* @param codes the codes to be used for message resolution
* @param arguments the arguments to be used for message resolution
* @param defaultMessage the default message to be used if no message is found
*/
public ErrorWrapper(Object error, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {
super(codes, arguments, defaultMessage);
Assert.notNull(error, "Error must not be null");
this.error = error;
}
/**
* Return the codes to be used for message resolution.
*
* @return the codes to be used for message resolution
*/
@Override
public String[] getCodes() {
return ((MessageSourceResolvable) this.error).getCodes();
}
/**
* Return the arguments to be used for message resolution.
*
* @return the arguments to be used for message resolution
*/
@Override
public Object[] getArguments() {
return ((MessageSourceResolvable) this.error).getArguments();
}
/**
* Return the default message to be used if no message is found.
*
* @return the default message to be used if no message is found
*/
@Override
public String getDefaultMessage() {
return ((MessageSourceResolvable) this.error).getDefaultMessage();
}
}

View File

@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.boot.web.error.ErrorWrapper;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.core.annotation.MergedAnnotation;
@@ -32,7 +33,6 @@ import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -48,8 +48,8 @@ import org.springframework.web.server.ServerWebExchange;
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception (if configured)</li>
* <li>message - The exception message (if configured)</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
* {@link MethodValidationResult} exception (if configured)</li>
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
* <li>trace - The exception stack trace (if configured)</li>
* <li>path - The URL path when the exception was raised</li>
* <li>requestId - Unique ID associated with the current request</li>
@@ -61,6 +61,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
* @author Yongjun Hong
* @since 2.0.0
* @see ErrorAttributes
*/
@@ -141,10 +142,9 @@ public class DefaultErrorAttributes implements ErrorAttributes {
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
MethodValidationResult result) {
List<ObjectError> errors = result.getAllErrors()
List<ErrorWrapper> errors = result.getAllErrors()
.stream()
.filter(ObjectError.class::isInstance)
.map(ObjectError.class::cast)
.map(ErrorWrapper::new)
.toList();
errorAttributes.put("message",
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());

View File

@@ -30,6 +30,7 @@ import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.error.ErrorWrapper;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
@@ -52,8 +53,8 @@ import org.springframework.web.servlet.ModelAndView;
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception (if configured)</li>
* <li>message - The exception message (if configured)</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} or
* {@link MethodValidationResult} exception (if configured)</li>
* <li>errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
* <li>trace - The exception stack trace (if configured)</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
@@ -65,6 +66,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
* @author Yongjun Hong
* @since 2.0.0
* @see ErrorAttributes
*/
@@ -153,6 +155,17 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
}
}
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
MethodValidationResult result) {
List<ErrorWrapper> errors = result.getAllErrors()
.stream()
.map(ErrorWrapper::new)
.toList();
errorAttributes.put("message",
"Validation failed for method='" + result.getMethod() + "'. Error count: " + errors.size());
errorAttributes.put("errors", errors);
}
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
errorAttributes.put("message", getMessage(webRequest, error));
}
@@ -187,16 +200,6 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
result.getAllErrors());
}
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
MethodValidationResult result) {
List<ObjectError> errors = result.getAllErrors()
.stream()
.filter(ObjectError.class::isInstance)
.map(ObjectError.class::cast)
.toList();
addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors);
}
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
List<ObjectError> errors) {
errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size());

View File

@@ -55,6 +55,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
* @author Yongjun Hong
*/
class DefaultErrorAttributesTests {
@@ -326,6 +327,30 @@ class DefaultErrorAttributesTests {
assertThat(attributes).doesNotContainKey("errors");
}
@Test
void extractParameterValidationResultErrors() throws Exception {
Object target = "test";
Method method = String.class.getMethod("substring", int.class);
MethodParameter parameter = new MethodParameter(method, 0);
ParameterValidationResult parameterValidationResult = new ParameterValidationResult(parameter, -1,
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
(error, sourceType) -> {
throw new IllegalArgumentException("No source object of the given type");
});
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
List.of(parameterValidationResult));
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, ex),
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
assertThat(attributes.get("message")).asString()
.isEqualTo("Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
assertThat(attributes).containsEntry("errors",
methodValidationResult.getAllErrors().stream().filter(ObjectError.class::isInstance).toList());
}
@Test
void excludeStatus() {
ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,