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
This commit is contained in:
parent
3c666ac4c8
commit
6b8d08a6e3
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* 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;
|
||||||
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
|
||||||
* @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 {
|
||||||
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() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* 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;
|
||||||
* 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 {
|
||||||
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) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* 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 @@
|
||||||
|
|
||||||
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
|
||||||
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;
|
||||||
* 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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue