From 13f00f7a8ac325fec230b86c269851f7e369796e Mon Sep 17 00:00:00 2001 From: yongjunhong Date: Sat, 30 Nov 2024 00:49:23 +0900 Subject: [PATCH 1/2] 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 See gh-43330 --- .../boot/web/error/ErrorWrapper.java | 90 +++++++++++++++++++ .../error/DefaultErrorAttributes.java | 12 +-- .../servlet/error/DefaultErrorAttributes.java | 27 +++--- .../error/DefaultErrorAttributesTests.java | 25 ++++++ 4 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java new file mode 100644 index 000000000000..e0d7e9d021b8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index c02b439fa9d5..db8bc3f8082a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -24,6 +24,7 @@ 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.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 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any {@link ObjectError}s from a {@link BindingResult} or - * {@link MethodValidationResult} exception (if configured)
  • + *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • *
  • requestId - Unique ID associated with the current request
  • @@ -61,6 +61,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong * @since 2.0.0 * @see ErrorAttributes */ @@ -141,10 +142,9 @@ else if (error instanceof ResponseStatusException responseStatusException) { private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, MethodValidationResult result) { - List errors = result.getAllErrors() + List 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()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index 9c351d633f79..fdeceb6ee62e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -30,6 +30,7 @@ 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 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any {@link ObjectError}s from a {@link BindingResult} or - * {@link MethodValidationResult} exception (if configured)
  • + *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • * @@ -65,6 +66,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong * @since 2.0.0 * @see ErrorAttributes */ @@ -153,6 +155,17 @@ private void addErrorMessage(Map errorAttributes, WebRequest web } } + private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, + MethodValidationResult result) { + List 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 errorAttributes, WebRequest webRequest, Throwable error) { errorAttributes.put("message", getMessage(webRequest, error)); } @@ -187,16 +200,6 @@ private void addMessageAndErrorsFromBindingResult(Map errorAttri result.getAllErrors()); } - private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, - MethodValidationResult result) { - List errors = result.getAllErrors() - .stream() - .filter(ObjectError.class::isInstance) - .map(ObjectError.class::cast) - .toList(); - addMessageAndErrorsForValidationFailure(errorAttributes, "method='" + result.getMethod() + "'", errors); - } - private void addMessageAndErrorsForValidationFailure(Map errorAttributes, String validated, List errors) { errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size()); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index f24de2fe8e5e..5ac81ae153c7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -55,6 +55,7 @@ * @author Scott Frederick * @author Moritz Halbritter * @author Yanming Zhou + * @author Yongjun Hong */ class DefaultErrorAttributesTests { @@ -326,6 +327,30 @@ void extractBindingResultErrorsExcludeMessageAndErrors() throws Exception { 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 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, From 977279b32ff48bfcdefa71699dbfd6dab3951b6f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 13 Jan 2025 17:35:50 -0800 Subject: [PATCH 2/2] Polish "Wrap 'error' attribute for consistent JSON serialization" Polish code and extend wrapping to all error types. See gh-43330 --- .../springframework/boot/web/error/Error.java | 109 ++++++++++++++++++ .../boot/web/error/ErrorWrapper.java | 90 --------------- .../error/DefaultErrorAttributes.java | 30 ++--- .../servlet/error/DefaultErrorAttributes.java | 49 +++----- .../error/DefaultErrorAttributesTests.java | 17 +-- .../error/DefaultErrorAttributesTests.java | 4 +- 6 files changed, 150 insertions(+), 149 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/Error.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/Error.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/Error.java new file mode 100644 index 000000000000..1ec7a4183021 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/Error.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2025 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.springframework.context.MessageSourceResolvable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * A wrapper class for {@link MessageSourceResolvable} errors that is safe for JSON + * serialization. + * + * @author Yongjun Hong + * @author Phillip Webb + * @since 3.5.0 + */ +public final class Error implements MessageSourceResolvable { + + private final MessageSourceResolvable cause; + + /** + * Create a new {@code Error} instance with the specified cause. + * @param cause the error cause (must not be {@code null}) + */ + private Error(MessageSourceResolvable cause) { + Assert.notNull(cause, "'cause' must not be null"); + this.cause = cause; + } + + @Override + public String[] getCodes() { + return this.cause.getCodes(); + } + + @Override + public Object[] getArguments() { + return this.cause.getArguments(); + } + + @Override + public String getDefaultMessage() { + return this.cause.getDefaultMessage(); + } + + /** + * Return the original cause of the error. + * @return the error cause + */ + public MessageSourceResolvable getCause() { + return this.cause; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return Objects.equals(this.cause, ((Error) obj).cause); + } + + @Override + public int hashCode() { + return Objects.hash(this.cause); + } + + @Override + public String toString() { + return this.cause.toString(); + } + + /** + * Wrap the given errors. + * @param errors the errors to wrap + * @return a new Error list + */ + public static List wrap(List errors) { + if (CollectionUtils.isEmpty(errors)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(errors.size()); + for (MessageSourceResolvable error : errors) { + result.add(new Error(error)); + } + return List.copyOf(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java deleted file mode 100644 index e0d7e9d021b8..000000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorWrapper.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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(); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index db8bc3f8082a..f574c35dd052 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -20,11 +20,10 @@ import java.io.StringWriter; import java.util.Date; import java.util.LinkedHashMap; -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.Error; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.error.ErrorAttributeOptions.Include; import org.springframework.core.annotation.MergedAnnotation; @@ -48,7 +47,7 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + *
  • errors - Any validation errors wrapped in {@link Error}, derived from a * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • @@ -113,19 +112,20 @@ private void handleException(Map errorAttributes, Throwable erro MergedAnnotation responseStatusAnnotation, boolean includeStackTrace) { Throwable exception; if (error instanceof BindingResult bindingResult) { - errorAttributes.put("message", error.getMessage()); - errorAttributes.put("errors", bindingResult.getAllErrors()); exception = error; + errorAttributes.put("message", error.getMessage()); + errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors())); } else if (error instanceof MethodValidationResult methodValidationResult) { - addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult); exception = error; + errorAttributes.put("message", getErrorMessage(methodValidationResult)); + errorAttributes.put("errors", Error.wrap(methodValidationResult.getAllErrors())); } else if (error instanceof ResponseStatusException responseStatusException) { - errorAttributes.put("message", responseStatusException.getReason()); exception = (responseStatusException.getCause() != null) ? responseStatusException.getCause() : error; + errorAttributes.put("message", responseStatusException.getReason()); if (exception instanceof BindingResult bindingResult) { - errorAttributes.put("errors", bindingResult.getAllErrors()); + errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors())); } } else { @@ -140,15 +140,9 @@ else if (error instanceof ResponseStatusException responseStatusException) { } } - private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, - MethodValidationResult result) { - List 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 String getErrorMessage(MethodValidationResult methodValidationResult) { + return "Validation failed for method='%s'. Error count: %s".formatted(methodValidationResult.getMethod(), + methodValidationResult.getAllErrors().size()); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index fdeceb6ee62e..0e29fd027321 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -20,7 +20,6 @@ import java.io.StringWriter; import java.util.Date; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import jakarta.servlet.RequestDispatcher; @@ -28,16 +27,15 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.boot.web.error.Error; 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; import org.springframework.util.ObjectUtils; 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.context.request.RequestAttributes; import org.springframework.web.context.request.WebRequest; @@ -53,7 +51,7 @@ *
  • error - The error reason
  • *
  • exception - The class name of the root exception (if configured)
  • *
  • message - The exception message (if configured)
  • - *
  • errors - Any validation errors wrapped in {@link ErrorWrapper}, derived from a + *
  • errors - Any validation errors wrapped in {@link Error}, derived from a * {@link BindingResult} or {@link MethodValidationResult} exception (if configured)
  • *
  • trace - The exception stack trace (if configured)
  • *
  • path - The URL path when the exception was raised
  • @@ -143,27 +141,27 @@ private void addErrorMessage(Map errorAttributes, WebRequest web BindingResult bindingResult = extractBindingResult(error); if (bindingResult != null) { addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult); + return; } - else { - MethodValidationResult methodValidationResult = extractMethodValidationResult(error); - if (methodValidationResult != null) { - addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult); - } - else { - addExceptionErrorMessage(errorAttributes, webRequest, error); - } + MethodValidationResult methodValidationResult = extractMethodValidationResult(error); + if (methodValidationResult != null) { + addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult); + return; } + addExceptionErrorMessage(errorAttributes, webRequest, error); + } + + private void addMessageAndErrorsFromBindingResult(Map errorAttributes, BindingResult result) { + errorAttributes.put("message", "Validation failed for object='%s'. Error count: %s" + .formatted(result.getObjectName(), result.getAllErrors().size())); + errorAttributes.put("errors", Error.wrap(result.getAllErrors())); } private void addMessageAndErrorsFromMethodValidationResult(Map errorAttributes, MethodValidationResult result) { - List 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); + errorAttributes.put("message", "Validation failed for method='%s'. Error count: %s" + .formatted(result.getMethod(), result.getAllErrors().size())); + errorAttributes.put("errors", Error.wrap(result.getAllErrors())); } private void addExceptionErrorMessage(Map errorAttributes, WebRequest webRequest, Throwable error) { @@ -195,17 +193,6 @@ protected String getMessage(WebRequest webRequest, Throwable error) { return "No message available"; } - private void addMessageAndErrorsFromBindingResult(Map errorAttributes, BindingResult result) { - addMessageAndErrorsForValidationFailure(errorAttributes, "object='" + result.getObjectName() + "'", - result.getAllErrors()); - } - - private void addMessageAndErrorsForValidationFailure(Map errorAttributes, String validated, - List errors) { - errorAttributes.put("message", "Validation failed for " + validated + ". Error count: " + errors.size()); - errorAttributes.put("errors", errors); - } - private BindingResult extractBindingResult(Throwable error) { if (error instanceof BindingResult bindingResult) { return bindingResult; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index 5ac81ae153c7..57dcd768c6a8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -273,7 +273,8 @@ void extractBindingResultErrors() throws Exception { .startsWith("Validation failed for argument at index 0 in method: " + "int org.springframework.boot.web.reactive.error.DefaultErrorAttributesTests" + ".method(java.lang.String), with 1 error(s)"); - assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors()); + assertThat(attributes).containsEntry("errors", + org.springframework.boot.web.error.Error.wrap(bindingResult.getAllErrors())); } @Test @@ -288,7 +289,8 @@ void extractBindingResultErrorsThatCausedAResponseStatusException() throws Excep buildServerRequest(request, new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid", ex)), ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS)); assertThat(attributes.get("message")).isEqualTo("Invalid"); - assertThat(attributes).containsEntry("errors", bindingResult.getAllErrors()); + assertThat(attributes).containsEntry("errors", + org.springframework.boot.web.error.Error.wrap(bindingResult.getAllErrors())); } @Test @@ -310,7 +312,7 @@ void extractMethodValidationResultErrors() throws Exception { .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()); + org.springframework.boot.web.error.Error.wrap(methodValidationResult.getAllErrors())); } @Test @@ -341,14 +343,13 @@ void extractParameterValidationResultErrors() throws Exception { List.of(parameterValidationResult)); HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult); MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); - Map 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"); + .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()); + org.springframework.boot.web.error.Error.wrap(methodValidationResult.getAllErrors())); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java index 999fb37d2001..992dbe3d54d7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -241,7 +241,7 @@ private void testErrors(List errors, String e assertThat(attributes).doesNotContainKey("message"); } if (options.isIncluded(Include.BINDING_ERRORS)) { - assertThat(attributes).containsEntry("errors", errors); + assertThat(attributes).containsEntry("errors", org.springframework.boot.web.error.Error.wrap(errors)); } else { assertThat(attributes).doesNotContainKey("errors");