Skip to content

Commit 6b8d08a

Browse files
Handle exceptions in management context
Prior to this commit, details about an exception would get dropped when the management context was separate from the application context and an actuator endpoint threw a binding exception. This commit adds some logic to capture the exception so the management context error handlers can add the appropriate attributes to the error response. Fixes gh-21036
1 parent 3c666ac commit 6b8d08a

File tree

3 files changed

+127
-25
lines changed

3 files changed

+127
-25
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) {
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.
@@ -16,22 +16,35 @@
1616

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

19+
import java.util.Collections;
20+
import java.util.Map;
21+
import java.util.function.Consumer;
22+
23+
import javax.validation.Valid;
24+
import javax.validation.constraints.NotEmpty;
25+
1926
import org.junit.jupiter.api.Test;
2027

2128
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
2229
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
2330
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
2431
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
2532
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
33+
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
2634
import org.springframework.boot.autoconfigure.AutoConfigurations;
2735
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
2836
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
2937
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;
3040
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
3141
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
3242
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
3343
import org.springframework.http.MediaType;
34-
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;
3548
import org.springframework.web.reactive.function.client.ClientResponse;
3649
import org.springframework.web.reactive.function.client.WebClient;
3750

@@ -41,29 +54,75 @@
4154
* Integration tests for {@link WebMvcEndpointChildContextConfiguration}.
4255
*
4356
* @author Phillip Webb
57+
* @author Scott Frederick
4458
*/
4559
class WebMvcEndpointChildContextConfigurationIntegrationTests {
4660

61+
final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(
62+
AnnotationConfigServletWebServerApplicationContext::new)
63+
.withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class,
64+
ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
65+
WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
66+
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class))
67+
.withUserConfiguration(FailingEndpoint.class, FailingControllerEndpoint.class)
68+
.withInitializer(new ServerPortInfoApplicationContextInitializer())
69+
.withPropertyValues("server.port=0", "management.server.port=0",
70+
"management.endpoints.web.exposure.include=*", "server.error.include-exception=true");
71+
4772
@Test // gh-17938
48-
void errorPageAndErrorControllerAreUsed() {
49-
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new)
50-
.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class,
51-
ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
52-
WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
53-
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class))
54-
.withUserConfiguration(FailingEndpoint.class)
55-
.withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues("server.port=0",
56-
"management.server.port=0", "management.endpoints.web.exposure.include=*")
57-
.run((context) -> {
58-
String port = context.getEnvironment().getProperty("local.management.port");
59-
WebClient client = WebClient.create("http://localhost:" + port);
60-
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON)
61-
.exchange().block();
62-
assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail");
63-
});
73+
void errorEndpointIsUsedWithEndpoint() {
74+
this.contextRunner.run(withWebTestClient((client) -> {
75+
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON).exchange()
76+
.block();
77+
Map<String, ?> body = getResponseBody(response);
78+
assertThat(body).hasEntrySatisfying("exception",
79+
(value) -> assertThat(value).asString().contains("IllegalStateException"));
80+
assertThat(body).hasEntrySatisfying("message",
81+
(value) -> assertThat(value).asString().contains("Epic Fail"));
82+
}));
83+
}
84+
85+
@Test
86+
void errorEndpointIsUsedWithRestControllerEndpoint() {
87+
this.contextRunner.run(withWebTestClient((client) -> {
88+
ClientResponse response = client.get().uri("actuator/failController").accept(MediaType.APPLICATION_JSON)
89+
.exchange().block();
90+
Map<String, ?> body = getResponseBody(response);
91+
assertThat(body).hasEntrySatisfying("exception",
92+
(value) -> assertThat(value).asString().contains("IllegalStateException"));
93+
assertThat(body).hasEntrySatisfying("message",
94+
(value) -> assertThat(value).asString().contains("Epic Fail"));
95+
}));
96+
}
97+
98+
@Test
99+
void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() {
100+
this.contextRunner.run(withWebTestClient((client) -> {
101+
ClientResponse response = client.post().uri("actuator/failController")
102+
.bodyValue(Collections.singletonMap("content", "")).accept(MediaType.APPLICATION_JSON).exchange()
103+
.block();
104+
Map<String, ?> body = getResponseBody(response);
105+
assertThat(body).hasEntrySatisfying("exception",
106+
(value) -> assertThat(value).asString().contains("MethodArgumentNotValidException"));
107+
assertThat(body).hasEntrySatisfying("message",
108+
(value) -> assertThat(value).asString().contains("Validation failed"));
109+
assertThat(body).hasEntrySatisfying("errors", (value) -> assertThat(value).asList().isNotEmpty());
110+
}));
111+
}
112+
113+
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
114+
return (context) -> {
115+
String port = context.getEnvironment().getProperty("local.management.port");
116+
WebClient client = WebClient.create("http://localhost:" + port);
117+
webClient.accept(client);
118+
};
119+
}
120+
121+
@SuppressWarnings("unchecked")
122+
private Map<String, ?> getResponseBody(ClientResponse response) {
123+
return (Map<String, ?>) response.bodyToMono(Map.class).block();
64124
}
65125

66-
@Component
67126
@Endpoint(id = "fail")
68127
static class FailingEndpoint {
69128

@@ -74,4 +133,35 @@ String fail() {
74133

75134
}
76135

136+
@RestControllerEndpoint(id = "failController")
137+
static class FailingControllerEndpoint {
138+
139+
@GetMapping
140+
String fail() {
141+
throw new IllegalStateException("Epic Fail");
142+
}
143+
144+
@PostMapping(produces = "application/json")
145+
@ResponseBody
146+
String bodyValidation(@Valid @RequestBody TestBody body) {
147+
return body.getContent();
148+
}
149+
150+
}
151+
152+
public static class TestBody {
153+
154+
@NotEmpty
155+
private String content;
156+
157+
public String getContent() {
158+
return this.content;
159+
}
160+
161+
public void setContent(String content) {
162+
this.content = content;
163+
}
164+
165+
}
166+
77167
}

0 commit comments

Comments
 (0)