Skip to content

Commit 0d00947

Browse files
Closes gh-21428
2 parents 64aac04 + 6b8d08a commit 0d00947

File tree

3 files changed

+120
-26
lines changed

3 files changed

+120
-26
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolver.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121
import java.util.Objects;
22+
import java.util.Optional;
2223

2324
import javax.servlet.http.HttpServletRequest;
2425
import javax.servlet.http.HttpServletResponse;
@@ -36,6 +37,7 @@
3637
* @author Andy Wilkinson
3738
* @author Stephane Nicoll
3839
* @author Phillip Webb
40+
* @author Scott Frederick
3941
*/
4042
class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
4143

@@ -50,8 +52,15 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp
5052
if (this.resolvers == null) {
5153
this.resolvers = extractResolvers();
5254
}
53-
return this.resolvers.stream().map((resolver) -> resolver.resolveException(request, response, handler, ex))
54-
.filter(Objects::nonNull).findFirst().orElse(null);
55+
Optional<ModelAndView> modelAndView = this.resolvers.stream()
56+
.map((resolver) -> resolver.resolveException(request, response, handler, ex)).filter(Objects::nonNull)
57+
.findFirst();
58+
modelAndView.ifPresent((mav) -> {
59+
if (mav.isEmpty()) {
60+
request.setAttribute("javax.servlet.error.exception", ex);
61+
}
62+
});
63+
return modelAndView.orElse(null);
5564
}
5665

5766
private List<HandlerExceptionResolver> extractResolvers() {

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/CompositeHandlerExceptionResolverTests.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@
3838
* Tests for {@link CompositeHandlerExceptionResolver}.
3939
*
4040
* @author Madhura Bhave
41+
* @author Scott Frederick
4142
*/
4243
class CompositeHandlerExceptionResolverTests {
4344

@@ -62,9 +63,11 @@ void resolverShouldAddDefaultResolverIfNonePresent() {
6263
load(BaseConfiguration.class);
6364
CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context
6465
.getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME);
65-
ModelAndView resolved = resolver.resolveException(this.request, this.response, null,
66-
new HttpRequestMethodNotSupportedException("POST"));
66+
HttpRequestMethodNotSupportedException exception = new HttpRequestMethodNotSupportedException("POST");
67+
ModelAndView resolved = resolver.resolveException(this.request, this.response, null, exception);
6768
assertThat(resolved).isNotNull();
69+
assertThat(resolved.isEmpty()).isTrue();
70+
assertThat(this.request.getAttribute("javax.servlet.error.exception")).isSameAs(exception);
6871
}
6972

7073
private void load(Class<?>... configs) {

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.web.servlet;
1818

19+
import java.util.Collections;
1920
import java.util.Map;
21+
import java.util.function.Consumer;
22+
23+
import javax.validation.Valid;
24+
import javax.validation.constraints.NotEmpty;
2025

2126
import org.junit.jupiter.api.Test;
2227

@@ -25,15 +30,21 @@
2530
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
2631
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
2732
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
33+
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
2834
import org.springframework.boot.autoconfigure.AutoConfigurations;
2935
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
3036
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
3137
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
38+
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
39+
import org.springframework.boot.test.context.runner.ContextConsumer;
3240
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
3341
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
3442
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
3543
import org.springframework.http.MediaType;
36-
import org.springframework.stereotype.Component;
44+
import org.springframework.web.bind.annotation.GetMapping;
45+
import org.springframework.web.bind.annotation.PostMapping;
46+
import org.springframework.web.bind.annotation.RequestBody;
47+
import org.springframework.web.bind.annotation.ResponseBody;
3748
import org.springframework.web.reactive.function.client.ClientResponse;
3849
import org.springframework.web.reactive.function.client.WebClient;
3950

@@ -54,40 +65,80 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
5465
ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class,
5566
EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
5667
ErrorMvcAutoConfiguration.class))
57-
.withUserConfiguration(FailingEndpoint.class)
58-
.withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues(
59-
"server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*");
68+
.withUserConfiguration(FailingEndpoint.class, FailingControllerEndpoint.class)
69+
.withInitializer(new ServerPortInfoApplicationContextInitializer())
70+
.withPropertyValues("server.port=0", "management.server.port=0",
71+
"management.endpoints.web.exposure.include=*", "server.error.include-exception=true",
72+
"server.error.include-message=always", "server.error.include-binding-errors=always");
6073

6174
@Test // gh-17938
62-
@SuppressWarnings("unchecked")
63-
void errorPageAndErrorControllerAreUsed() {
64-
this.runner.run((context) -> {
65-
String port = context.getEnvironment().getProperty("local.management.port");
66-
WebClient client = WebClient.create("http://localhost:" + port);
75+
void errorEndpointIsUsedWithEndpoint() {
76+
this.runner.run(withWebTestClient((client) -> {
6777
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON).exchange()
6878
.block();
69-
Map<Object, Object> body = response.bodyToMono(Map.class).block();
70-
assertThat(body).containsEntry("message", "");
71-
assertThat(body).doesNotContainKey("trace");
72-
});
79+
Map<String, ?> body = getResponseBody(response);
80+
assertThat(body).hasEntrySatisfying("exception",
81+
(value) -> assertThat(value).asString().contains("IllegalStateException"));
82+
assertThat(body).hasEntrySatisfying("message",
83+
(value) -> assertThat(value).asString().contains("Epic Fail"));
84+
}));
7385
}
7486

7587
@Test
7688
void errorPageAndErrorControllerIncludeDetails() {
7789
this.runner.withPropertyValues("server.error.include-stacktrace=always", "server.error.include-message=always")
78-
.run((context) -> {
79-
String port = context.getEnvironment().getProperty("local.management.port");
80-
WebClient client = WebClient.create("http://localhost:" + port);
90+
.run(withWebTestClient((client) -> {
8191
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON)
8292
.exchange().block();
83-
Map<Object, Object> body = response.bodyToMono(Map.class).block();
84-
assertThat(body).containsEntry("message", "Epic Fail");
93+
Map<String, ?> body = getResponseBody(response);
94+
assertThat(body).hasEntrySatisfying("message",
95+
(value) -> assertThat(value).asString().contains("Epic Fail"));
8596
assertThat(body).hasEntrySatisfying("trace", (value) -> assertThat(value).asString()
8697
.contains("java.lang.IllegalStateException: Epic Fail"));
87-
});
98+
}));
99+
}
100+
101+
@Test
102+
void errorEndpointIsUsedWithRestControllerEndpoint() {
103+
this.runner.run(withWebTestClient((client) -> {
104+
ClientResponse response = client.get().uri("actuator/failController").accept(MediaType.APPLICATION_JSON)
105+
.exchange().block();
106+
Map<String, ?> body = getResponseBody(response);
107+
assertThat(body).hasEntrySatisfying("exception",
108+
(value) -> assertThat(value).asString().contains("IllegalStateException"));
109+
assertThat(body).hasEntrySatisfying("message",
110+
(value) -> assertThat(value).asString().contains("Epic Fail"));
111+
}));
112+
}
113+
114+
@Test
115+
void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() {
116+
this.runner.run(withWebTestClient((client) -> {
117+
ClientResponse response = client.post().uri("actuator/failController")
118+
.bodyValue(Collections.singletonMap("content", "")).accept(MediaType.APPLICATION_JSON).exchange()
119+
.block();
120+
Map<String, ?> body = getResponseBody(response);
121+
assertThat(body).hasEntrySatisfying("exception",
122+
(value) -> assertThat(value).asString().contains("MethodArgumentNotValidException"));
123+
assertThat(body).hasEntrySatisfying("message",
124+
(value) -> assertThat(value).asString().contains("Validation failed"));
125+
assertThat(body).hasEntrySatisfying("errors", (value) -> assertThat(value).asList().isNotEmpty());
126+
}));
127+
}
128+
129+
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
130+
return (context) -> {
131+
String port = context.getEnvironment().getProperty("local.management.port");
132+
WebClient client = WebClient.create("http://localhost:" + port);
133+
webClient.accept(client);
134+
};
135+
}
136+
137+
@SuppressWarnings("unchecked")
138+
private Map<String, ?> getResponseBody(ClientResponse response) {
139+
return (Map<String, ?>) response.bodyToMono(Map.class).block();
88140
}
89141

90-
@Component
91142
@Endpoint(id = "fail")
92143
static class FailingEndpoint {
93144

@@ -98,4 +149,35 @@ String fail() {
98149

99150
}
100151

152+
@RestControllerEndpoint(id = "failController")
153+
static class FailingControllerEndpoint {
154+
155+
@GetMapping
156+
String fail() {
157+
throw new IllegalStateException("Epic Fail");
158+
}
159+
160+
@PostMapping(produces = "application/json")
161+
@ResponseBody
162+
String bodyValidation(@Valid @RequestBody TestBody body) {
163+
return body.getContent();
164+
}
165+
166+
}
167+
168+
public static class TestBody {
169+
170+
@NotEmpty
171+
private String content;
172+
173+
public String getContent() {
174+
return this.content;
175+
}
176+
177+
public void setContent(String content) {
178+
this.content = content;
179+
}
180+
181+
}
182+
101183
}

0 commit comments

Comments
 (0)