Commit 6b8d08a6 authored by Scott Frederick's avatar Scott Frederick

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
parent 3c666ac4
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; ...@@ -19,6 +19,7 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
...@@ -36,6 +37,7 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv ...@@ -36,6 +37,7 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class CompositeHandlerExceptionResolver implements HandlerExceptionResolver { class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
...@@ -50,8 +52,15 @@ class CompositeHandlerExceptionResolver implements HandlerExceptionResolver { ...@@ -50,8 +52,15 @@ class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
if (this.resolvers == null) { if (this.resolvers == null) {
this.resolvers = extractResolvers(); this.resolvers = extractResolvers();
} }
return this.resolvers.stream().map((resolver) -> resolver.resolveException(request, response, handler, ex)) Optional<ModelAndView> modelAndView = this.resolvers.stream()
.filter(Objects::nonNull).findFirst().orElse(null); .map((resolver) -> resolver.resolveException(request, response, handler, ex)).filter(Objects::nonNull)
.findFirst();
modelAndView.ifPresent((mav) -> {
if (mav.isEmpty()) {
request.setAttribute("javax.servlet.error.exception", ex);
}
});
return modelAndView.orElse(null);
} }
private List<HandlerExceptionResolver> extractResolvers() { private List<HandlerExceptionResolver> extractResolvers() {
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link CompositeHandlerExceptionResolver}. * Tests for {@link CompositeHandlerExceptionResolver}.
* *
* @author Madhura Bhave * @author Madhura Bhave
* @author Scott Frederick
*/ */
class CompositeHandlerExceptionResolverTests { class CompositeHandlerExceptionResolverTests {
...@@ -62,9 +63,11 @@ class CompositeHandlerExceptionResolverTests { ...@@ -62,9 +63,11 @@ class CompositeHandlerExceptionResolverTests {
load(BaseConfiguration.class); load(BaseConfiguration.class);
CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context
.getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME); .getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME);
ModelAndView resolved = resolver.resolveException(this.request, this.response, null, HttpRequestMethodNotSupportedException exception = new HttpRequestMethodNotSupportedException("POST");
new HttpRequestMethodNotSupportedException("POST")); ModelAndView resolved = resolver.resolveException(this.request, this.response, null, exception);
assertThat(resolved).isNotNull(); assertThat(resolved).isNotNull();
assertThat(resolved.isEmpty()).isTrue();
assertThat(this.request.getAttribute("javax.servlet.error.exception")).isSameAs(exception);
} }
private void load(Class<?>... configs) { private void load(Class<?>... configs) {
......
/* /*
* Copyright 2012-2019 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -16,6 +16,13 @@ ...@@ -16,6 +16,13 @@
package org.springframework.boot.actuate.autoconfigure.web.servlet; package org.springframework.boot.actuate.autoconfigure.web.servlet;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
...@@ -23,15 +30,21 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu ...@@ -23,15 +30,21 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAu
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
...@@ -41,29 +54,75 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -41,29 +54,75 @@ import static org.assertj.core.api.Assertions.assertThat;
* Integration tests for {@link WebMvcEndpointChildContextConfiguration}. * Integration tests for {@link WebMvcEndpointChildContextConfiguration}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class WebMvcEndpointChildContextConfigurationIntegrationTests { class WebMvcEndpointChildContextConfigurationIntegrationTests {
final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class,
ManagementContextAutoConfiguration.class, ServletManagementContextAutoConfiguration.class,
WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class))
.withUserConfiguration(FailingEndpoint.class, FailingControllerEndpoint.class)
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withPropertyValues("server.port=0", "management.server.port=0",
"management.endpoints.web.exposure.include=*", "server.error.include-exception=true");
@Test // gh-17938 @Test // gh-17938
void errorPageAndErrorControllerAreUsed() { void errorEndpointIsUsedWithEndpoint() {
new WebApplicationContextRunner(AnnotationConfigServletWebServerApplicationContext::new) this.contextRunner.run(withWebTestClient((client) -> {
.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON).exchange()
ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, .block();
WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, Map<String, ?> body = getResponseBody(response);
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class)) assertThat(body).hasEntrySatisfying("exception",
.withUserConfiguration(FailingEndpoint.class) (value) -> assertThat(value).asString().contains("IllegalStateException"));
.withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues("server.port=0", assertThat(body).hasEntrySatisfying("message",
"management.server.port=0", "management.endpoints.web.exposure.include=*") (value) -> assertThat(value).asString().contains("Epic Fail"));
.run((context) -> { }));
String port = context.getEnvironment().getProperty("local.management.port"); }
WebClient client = WebClient.create("http://localhost:" + port);
ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON) @Test
.exchange().block(); void errorEndpointIsUsedWithRestControllerEndpoint() {
assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail"); this.contextRunner.run(withWebTestClient((client) -> {
}); ClientResponse response = client.get().uri("actuator/failController").accept(MediaType.APPLICATION_JSON)
.exchange().block();
Map<String, ?> body = getResponseBody(response);
assertThat(body).hasEntrySatisfying("exception",
(value) -> assertThat(value).asString().contains("IllegalStateException"));
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Epic Fail"));
}));
}
@Test
void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() {
this.contextRunner.run(withWebTestClient((client) -> {
ClientResponse response = client.post().uri("actuator/failController")
.bodyValue(Collections.singletonMap("content", "")).accept(MediaType.APPLICATION_JSON).exchange()
.block();
Map<String, ?> body = getResponseBody(response);
assertThat(body).hasEntrySatisfying("exception",
(value) -> assertThat(value).asString().contains("MethodArgumentNotValidException"));
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Validation failed"));
assertThat(body).hasEntrySatisfying("errors", (value) -> assertThat(value).asList().isNotEmpty());
}));
}
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
return (context) -> {
String port = context.getEnvironment().getProperty("local.management.port");
WebClient client = WebClient.create("http://localhost:" + port);
webClient.accept(client);
};
}
@SuppressWarnings("unchecked")
private Map<String, ?> getResponseBody(ClientResponse response) {
return (Map<String, ?>) response.bodyToMono(Map.class).block();
} }
@Component
@Endpoint(id = "fail") @Endpoint(id = "fail")
static class FailingEndpoint { static class FailingEndpoint {
...@@ -74,4 +133,35 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests { ...@@ -74,4 +133,35 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
} }
@RestControllerEndpoint(id = "failController")
static class FailingControllerEndpoint {
@GetMapping
String fail() {
throw new IllegalStateException("Epic Fail");
}
@PostMapping(produces = "application/json")
@ResponseBody
String bodyValidation(@Valid @RequestBody TestBody body) {
return body.getContent();
}
}
public static class TestBody {
@NotEmpty
private String content;
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment