Skip to content

Commit 0db3232

Browse files
committed
Merge branch '3.5.x'
Closes gh-46311
2 parents a111b14 + efca113 commit 0db3232

File tree

6 files changed

+101
-18
lines changed

6 files changed

+101
-18
lines changed

spring-boot-project/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/error/DefaultErrorAttributes.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
* <li>error - The error reason</li>
4848
* <li>exception - The class name of the root exception (if configured)</li>
4949
* <li>message - The exception message (if configured)</li>
50-
* <li>errors - Any validation errors wrapped in {@link Error}, derived from a
51-
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
50+
* <li>errors - Any validation errors derived from a {@link BindingResult} or
51+
* {@link MethodValidationResult} exception (if configured). To ensure safe serialization
52+
* to JSON, errors are {@link Error#wrapIfNecessary(java.util.List) wrapped if
53+
* necessary}</li>
5254
* <li>trace - The exception stack trace (if configured)</li>
5355
* <li>path - The URL path when the exception was raised</li>
5456
* <li>requestId - Unique ID associated with the current request</li>
@@ -114,18 +116,18 @@ private void handleException(Map<String, Object> errorAttributes, Throwable erro
114116
if (error instanceof BindingResult bindingResult) {
115117
exception = error;
116118
errorAttributes.put("message", error.getMessage());
117-
errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors()));
119+
errorAttributes.put("errors", Error.wrapIfNecessary(bindingResult.getAllErrors()));
118120
}
119121
else if (error instanceof MethodValidationResult methodValidationResult) {
120122
exception = error;
121123
errorAttributes.put("message", getErrorMessage(methodValidationResult));
122-
errorAttributes.put("errors", Error.wrap(methodValidationResult.getAllErrors()));
124+
errorAttributes.put("errors", Error.wrapIfNecessary(methodValidationResult.getAllErrors()));
123125
}
124126
else if (error instanceof ResponseStatusException responseStatusException) {
125127
exception = (responseStatusException.getCause() != null) ? responseStatusException.getCause() : error;
126128
errorAttributes.put("message", responseStatusException.getReason());
127129
if (exception instanceof BindingResult bindingResult) {
128-
errorAttributes.put("errors", Error.wrap(bindingResult.getAllErrors()));
130+
errorAttributes.put("errors", Error.wrapIfNecessary(bindingResult.getAllErrors()));
129131
}
130132
}
131133
else {

spring-boot-project/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/error/DefaultErrorAttributesTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ void extractBindingResultErrors() throws Exception {
273273
.startsWith("Validation failed for argument at index 0 in method: " + "int " + getClass().getName()
274274
+ ".method(java.lang.String), with 1 error(s)");
275275
assertThat(attributes).containsEntry("errors",
276-
org.springframework.boot.web.error.Error.wrap(bindingResult.getAllErrors()));
276+
org.springframework.boot.web.error.Error.wrapIfNecessary(bindingResult.getAllErrors()));
277277
}
278278

279279
@Test
@@ -289,7 +289,7 @@ void extractBindingResultErrorsThatCausedAResponseStatusException() throws Excep
289289
ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
290290
assertThat(attributes.get("message")).isEqualTo("Invalid");
291291
assertThat(attributes).containsEntry("errors",
292-
org.springframework.boot.web.error.Error.wrap(bindingResult.getAllErrors()));
292+
org.springframework.boot.web.error.Error.wrapIfNecessary(bindingResult.getAllErrors()));
293293
}
294294

295295
@Test
@@ -311,7 +311,7 @@ void extractMethodValidationResultErrors() throws Exception {
311311
.isEqualTo(
312312
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
313313
assertThat(attributes).containsEntry("errors",
314-
org.springframework.boot.web.error.Error.wrap(methodValidationResult.getAllErrors()));
314+
org.springframework.boot.web.error.Error.wrapIfNecessary(methodValidationResult.getAllErrors()));
315315
}
316316

317317
@Test
@@ -348,7 +348,7 @@ void extractParameterValidationResultErrors() throws Exception {
348348
.isEqualTo(
349349
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1");
350350
assertThat(attributes).containsEntry("errors",
351-
org.springframework.boot.web.error.Error.wrap(methodValidationResult.getAllErrors()));
351+
org.springframework.boot.web.error.Error.wrapIfNecessary(methodValidationResult.getAllErrors()));
352352
}
353353

354354
@Test

spring-boot-project/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/error/DefaultErrorAttributes.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,10 @@
5151
* <li>error - The error reason</li>
5252
* <li>exception - The class name of the root exception (if configured)</li>
5353
* <li>message - The exception message (if configured)</li>
54-
* <li>errors - Any validation errors wrapped in {@link Error}, derived from a
55-
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
54+
* <li>errors - Any validation errors derived from a {@link BindingResult} or
55+
* {@link MethodValidationResult} exception (if configured). To ensure safe serialization
56+
* to JSON, errors are {@link Error#wrapIfNecessary(java.util.List) wrapped if
57+
* necessary}</li>
5658
* <li>trace - The exception stack trace (if configured)</li>
5759
* <li>path - The URL path when the exception was raised</li>
5860
* </ul>
@@ -154,14 +156,14 @@ private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest web
154156
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
155157
errorAttributes.put("message", "Validation failed for object='%s'. Error count: %s"
156158
.formatted(result.getObjectName(), result.getAllErrors().size()));
157-
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
159+
errorAttributes.put("errors", Error.wrapIfNecessary(result.getAllErrors()));
158160
}
159161

160162
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
161163
MethodValidationResult result) {
162164
errorAttributes.put("message", "Validation failed for method='%s'. Error count: %s"
163165
.formatted(result.getMethod(), result.getAllErrors().size()));
164-
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
166+
errorAttributes.put("errors", Error.wrapIfNecessary(result.getAllErrors()));
165167
}
166168

167169
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {

spring-boot-project/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/error/DefaultErrorAttributesTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ private void testErrors(List<? extends MessageSourceResolvable> errors, String e
241241
assertThat(attributes).doesNotContainKey("message");
242242
}
243243
if (options.isIncluded(Include.BINDING_ERRORS)) {
244-
assertThat(attributes).containsEntry("errors", org.springframework.boot.web.error.Error.wrap(errors));
244+
assertThat(attributes).containsEntry("errors",
245+
org.springframework.boot.web.error.Error.wrapIfNecessary(errors));
245246
}
246247
else {
247248
assertThat(attributes).doesNotContainKey("errors");

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/Error.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.util.List;
2222
import java.util.Objects;
2323

24+
import com.fasterxml.jackson.annotation.JsonIgnore;
25+
2426
import org.springframework.context.MessageSourceResolvable;
2527
import org.springframework.util.Assert;
2628
import org.springframework.util.CollectionUtils;
29+
import org.springframework.validation.ObjectError;
2730

2831
/**
2932
* A wrapper class for {@link MessageSourceResolvable} errors that is safe for JSON
@@ -65,6 +68,7 @@ public String getDefaultMessage() {
6568
* Return the original cause of the error.
6669
* @return the error cause
6770
*/
71+
@JsonIgnore
6872
public MessageSourceResolvable getCause() {
6973
return this.cause;
7074
}
@@ -91,19 +95,26 @@ public String toString() {
9195
}
9296

9397
/**
94-
* Wrap the given errors.
98+
* Wrap the given errors, if necessary, such that they are suitable for serialization
99+
* to JSON. {@link MessageSourceResolvable} implementations that are known to be
100+
* suitable are not wrapped.
95101
* @param errors the errors to wrap
96102
* @return a new Error list
103+
* @since 3.5.4
97104
*/
98-
public static List<Error> wrap(List<? extends MessageSourceResolvable> errors) {
105+
public static List<MessageSourceResolvable> wrapIfNecessary(List<? extends MessageSourceResolvable> errors) {
99106
if (CollectionUtils.isEmpty(errors)) {
100107
return Collections.emptyList();
101108
}
102-
List<Error> result = new ArrayList<>(errors.size());
109+
List<MessageSourceResolvable> result = new ArrayList<>(errors.size());
103110
for (MessageSourceResolvable error : errors) {
104-
result.add(new Error(error));
111+
result.add(requiresWrapping(error) ? new Error(error) : error);
105112
}
106113
return List.copyOf(result);
107114
}
108115

116+
private static boolean requiresWrapping(MessageSourceResolvable error) {
117+
return !(error instanceof ObjectError);
118+
}
119+
109120
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.error;
18+
19+
import java.util.List;
20+
21+
import com.fasterxml.jackson.core.JsonProcessingException;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import org.junit.jupiter.api.Test;
24+
25+
import org.springframework.context.MessageSourceResolvable;
26+
import org.springframework.context.support.DefaultMessageSourceResolvable;
27+
import org.springframework.validation.FieldError;
28+
import org.springframework.validation.ObjectError;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* Tests for {@link Error}.
34+
*
35+
* @author Andy Wilkinson
36+
*/
37+
class ErrorTests {
38+
39+
@Test
40+
@SuppressWarnings("rawtypes")
41+
void wrapIfNecessaryDoesNotWrapFieldErrorOrObjectError() {
42+
List<MessageSourceResolvable> wrapped = Error.wrapIfNecessary(List.of(new ObjectError("name", "message"),
43+
new FieldError("name", "field", "message"), new CustomMessageSourceResolvable("code")));
44+
assertThat(wrapped).extracting((error) -> (Class) error.getClass())
45+
.containsExactly(ObjectError.class, FieldError.class, Error.class);
46+
}
47+
48+
@Test
49+
void errorCauseDoesNotAppearInJson() throws JsonProcessingException {
50+
String json = new ObjectMapper()
51+
.writeValueAsString(Error.wrapIfNecessary(List.of(new CustomMessageSourceResolvable("code"))));
52+
assertThat(json).doesNotContain("some detail");
53+
}
54+
55+
public static class CustomMessageSourceResolvable extends DefaultMessageSourceResolvable {
56+
57+
CustomMessageSourceResolvable(String code) {
58+
super(code);
59+
}
60+
61+
public String getDetail() {
62+
return "some detail";
63+
}
64+
65+
}
66+
67+
}

0 commit comments

Comments
 (0)