Polish "Wrap 'error' attribute for consistent JSON serialization"
Polish code and extend wrapping to all error types. See gh-43330
This commit is contained in:
parent
13f00f7a8a
commit
977279b32f
|
|
@ -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<Error> wrap(List<? extends MessageSourceResolvable> errors) {
|
||||
if (CollectionUtils.isEmpty(errors)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Error> result = new ArrayList<>(errors.size());
|
||||
for (MessageSourceResolvable error : errors) {
|
||||
result.add(new Error(error));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.PrintWriter;
|
|||
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 @@ 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 validation errors wrapped in {@link ErrorWrapper}, derived from a
|
||||
* <li>errors - Any validation errors wrapped in {@link Error}, 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>
|
||||
|
|
@ -113,19 +112,20 @@ public class DefaultErrorAttributes implements ErrorAttributes {
|
|||
MergedAnnotation<ResponseStatus> 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 @@ public class DefaultErrorAttributes implements ErrorAttributes {
|
|||
}
|
||||
}
|
||||
|
||||
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 String getErrorMessage(MethodValidationResult methodValidationResult) {
|
||||
return "Validation failed for method='%s'. Error count: %s".formatted(methodValidationResult.getMethod(),
|
||||
methodValidationResult.getAllErrors().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -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.PrintWriter;
|
|||
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.ServletException;
|
|||
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 @@ 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 validation errors wrapped in {@link ErrorWrapper}, derived from a
|
||||
* <li>errors - Any validation errors wrapped in {@link Error}, 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>
|
||||
|
|
@ -143,27 +141,27 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
|
|||
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<String, Object> 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<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);
|
||||
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<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
|
||||
|
|
@ -195,17 +193,6 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
|
|||
return "No message available";
|
||||
}
|
||||
|
||||
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
|
||||
addMessageAndErrorsForValidationFailure(errorAttributes, "object='" + result.getObjectName() + "'",
|
||||
result.getAllErrors());
|
||||
}
|
||||
|
||||
private void addMessageAndErrorsForValidationFailure(Map<String, Object> errorAttributes, String validated,
|
||||
List<ObjectError> 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;
|
||||
|
|
|
|||
|
|
@ -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 @@ class DefaultErrorAttributesTests {
|
|||
.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 @@ class DefaultErrorAttributesTests {
|
|||
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 @@ class DefaultErrorAttributesTests {
|
|||
.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 @@ class DefaultErrorAttributesTests {
|
|||
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");
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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 @@ class DefaultErrorAttributesTests {
|
|||
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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue