Create spring-boot-webmvc module

This commit is contained in:
Andy Wilkinson
2025-03-27 14:05:55 +00:00
committed by Phillip Webb
parent e83f4eed4e
commit dfd5e56956
129 changed files with 450 additions and 360 deletions

View File

@@ -0,0 +1,288 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DispatcherServletAutoConfiguration}.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Brian Clozel
*/
class DispatcherServletAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class));
@Test
void registrationProperties() {
this.contextRunner.run((context) -> {
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
});
}
@Test
void registrationNonServletBean() {
this.contextRunner.withUserConfiguration(NonServletConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(ServletRegistrationBean.class);
assertThat(context).doesNotHaveBean(DispatcherServlet.class);
assertThat(context).doesNotHaveBean(DispatcherServletPath.class);
});
}
@Test
void registrationOverrideWithDefaultDispatcherServletNameResultsInSingleDispatcherServlet() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(1);
});
}
@Test
void registrationOverrideWithNonDefaultDispatcherServletNameResultsInTwoDispatcherServlets() {
this.contextRunner
.withUserConfiguration(CustomDispatcherServletDifferentName.class, CustomDispatcherServletPath.class)
.run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(2);
});
}
@Test
void registrationOverrideWithAutowiredServlet() {
this.contextRunner.withUserConfiguration(CustomAutowiredRegistration.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/foo");
assertThat(registration.getServletName()).isEqualTo("customDispatcher");
assertThat(context).hasSingleBean(DispatcherServlet.class);
});
}
@Test
void servletPath() {
this.contextRunner.withPropertyValues("spring.mvc.servlet.path:/spring").run((context) -> {
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/spring/*");
assertThat(registration.getMultipartConfig()).isNull();
assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring");
});
}
@Test
void dispatcherServletPathWhenCustomDispatcherServletSameNameShouldReturnConfiguredServletPath() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class)
.withPropertyValues("spring.mvc.servlet.path:/spring")
.run((context) -> assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring"));
}
@Test
void dispatcherServletPathNotCreatedWhenDefaultDispatcherServletNotAvailable() {
this.contextRunner
.withUserConfiguration(CustomDispatcherServletDifferentName.class, NonServletConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
}
@Test
void dispatcherServletPathNotCreatedWhenCustomRegistrationBeanPresent() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletRegistration.class)
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
}
@Test
void multipartConfig() {
this.contextRunner.withUserConfiguration(MultipartConfiguration.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getMultipartConfig()).isNotNull();
});
}
@Test
void renamesMultipartResolver() {
this.contextRunner.withUserConfiguration(MultipartResolverConfiguration.class).run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
dispatcherServlet.onApplicationEvent(new ContextRefreshedEvent(context));
assertThat(dispatcherServlet.getMultipartResolver()).isInstanceOf(MockMultipartResolver.class);
});
}
@Test
void dispatcherServletDefaultConfig() {
this.contextRunner.run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(true);
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(false);
assertThat(dispatcherServlet).extracting("enableLoggingRequestDetails").isEqualTo(false);
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(true);
assertThat(context.getBean("dispatcherServletRegistration")).hasFieldOrPropertyWithValue("loadOnStartup",
-1);
});
}
@Test
void dispatcherServletCustomConfig() {
this.contextRunner
.withPropertyValues("spring.mvc.dispatch-options-request:false", "spring.mvc.dispatch-trace-request:true",
"spring.mvc.publish-request-handled-events:false", "spring.mvc.servlet.load-on-startup=5")
.run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(false);
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(true);
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(false);
assertThat(context.getBean("dispatcherServletRegistration"))
.hasFieldOrPropertyWithValue("loadOnStartup", 5);
});
}
@Configuration(proxyBeanMethods = false)
static class MultipartConfiguration {
@Bean
MultipartConfigElement multipartConfig() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofKilobytes(128));
factory.setMaxRequestSize(DataSize.ofKilobytes(128));
return factory.createMultipartConfig();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletDifferentName {
@Bean
DispatcherServlet customDispatcherServlet() {
return new DispatcherServlet();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletPath {
@Bean
DispatcherServletPath dispatcherServletPath() {
return mock(DispatcherServletPath.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomAutowiredRegistration {
@Bean
ServletRegistrationBean<?> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
"/foo");
registration.setName("customDispatcher");
return registration;
}
@Bean
DispatcherServletPath dispatcherServletPath() {
return mock(DispatcherServletPath.class);
}
}
@Configuration(proxyBeanMethods = false)
static class NonServletConfiguration {
@Bean
String dispatcherServlet() {
return "spring";
}
}
@Configuration(proxyBeanMethods = false)
static class MultipartResolverConfiguration {
@Bean
MultipartResolver getMultipartResolver() {
return new MockMultipartResolver();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletSameName {
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletRegistration {
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
"/foo");
registration.setName("customDispatcher");
return registration;
}
}
static class MockMultipartResolver implements MultipartResolver {
@Override
public boolean isMultipart(HttpServletRequest request) {
return false;
}
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {
return null;
}
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DispatcherServletPath}.
*
* @author Phillip Webb
*/
class DispatcherServletPathTests {
@Test
void getRelativePathReturnsRelativePath() {
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((DispatcherServletPath) () -> "spring/").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("/boot")).isEqualTo("spring/boot");
}
@Test
void getPrefixWhenHasSimplePathReturnPath() {
assertThat(((DispatcherServletPath) () -> "spring").getPrefix()).isEqualTo("spring");
}
@Test
void getPrefixWhenHasPatternRemovesPattern() {
assertThat(((DispatcherServletPath) () -> "spring/*.do").getPrefix()).isEqualTo("spring");
}
@Test
void getPathWhenPathEndsWithSlashRemovesSlash() {
assertThat(((DispatcherServletPath) () -> "spring/").getPrefix()).isEqualTo("spring");
}
@Test
void getServletUrlMappingWhenPathIsEmptyReturnsSlash() {
assertThat(((DispatcherServletPath) () -> "").getServletUrlMapping()).isEqualTo("/");
}
@Test
void getServletUrlMappingWhenPathIsSlashReturnsSlash() {
assertThat(((DispatcherServletPath) () -> "/").getServletUrlMapping()).isEqualTo("/");
}
@Test
void getServletUrlMappingWhenPathContainsStarReturnsPath() {
assertThat(((DispatcherServletPath) () -> "spring/*.do").getServletUrlMapping()).isEqualTo("spring/*.do");
}
@Test
void getServletUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() {
assertThat(((DispatcherServletPath) () -> "spring/boot").getServletUrlMapping()).isEqualTo("spring/boot/*");
}
@Test
void getServletUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() {
assertThat(((DispatcherServletPath) () -> "spring/boot/").getServletUrlMapping()).isEqualTo("spring/boot/*");
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link DispatcherServletRegistrationBean}.
*
* @author Phillip Webb
*/
class DispatcherServletRegistrationBeanTests {
@Test
void createWhenPathIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DispatcherServletRegistrationBean(new DispatcherServlet(), null))
.withMessageContaining("'path' must not be null");
}
@Test
void getPathReturnsPath() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThat(bean.getPath()).isEqualTo("/test");
}
@Test
void getUrlMappingsReturnsSinglePathMappedPattern() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThat(bean.getUrlMappings()).containsOnly("/test/*");
}
@Test
void setUrlMappingsCannotBeCalled() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> bean.setUrlMappings(Collections.emptyList()));
}
@Test
void addUrlMappingsCannotBeCalled() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> bean.addUrlMappings("/test"));
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JspTemplateAvailabilityProvider}.
*
* @author Yunkun Huang
*/
class JspTemplateAvailabilityProviderTests {
private final JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider();
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
private final MockEnvironment environment = new MockEnvironment();
@Test
void availabilityOfTemplateThatDoesNotExist() {
assertThat(isTemplateAvailable("whatever")).isFalse();
}
@Test
@WithResource(name = "custom-templates/custom.jsp")
void availabilityOfTemplateWithCustomPrefix() {
this.environment.setProperty("spring.mvc.view.prefix", "classpath:/custom-templates/");
assertThat(isTemplateAvailable("custom.jsp")).isTrue();
}
@Test
@WithResource(name = "suffixed.java-server-pages")
void availabilityOfTemplateWithCustomSuffix() {
this.environment.setProperty("spring.mvc.view.suffix", ".java-server-pages");
assertThat(isTemplateAvailable("suffixed")).isTrue();
}
private boolean isTemplateAvailable(String view) {
return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(),
this.resourceLoader);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import java.util.Collections;
import java.util.Map;
import org.assertj.core.util.Throwables;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link WebMvcProperties}.
*
* @author Stephane Nicoll
*/
class WebMvcPropertiesTests {
private final WebMvcProperties properties = new WebMvcProperties();
@Test
void servletPathWhenEndsWithSlashHasValidMappingAndPrefix() {
bind("spring.mvc.servlet.path", "/foo/");
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
}
@Test
void servletPathWhenDoesNotEndWithSlashHasValidMappingAndPrefix() {
bind("spring.mvc.servlet.path", "/foo");
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
}
@Test
void servletPathWhenHasWildcardThrowsException() {
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind("spring.mvc.servlet.path", "/*"))
.withRootCauseInstanceOf(IllegalArgumentException.class)
.satisfies((ex) -> assertThat(Throwables.getRootCause(ex)).hasMessage("'path' must not contain wildcards"));
}
private void bind(String name, String value) {
bind(Collections.singletonMap(name, value));
}
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.mvc", Bindable.ofInstance(this.properties));
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
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.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.AbstractView;
import org.springframework.web.servlet.view.InternalResourceView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WelcomePageHandlerMapping}.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
class WelcomePageHandlerMappingTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(HandlerMappingConfiguration.class)
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
@Test
void isOrderedAtLowPriority() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
WelcomePageHandlerMapping handler = context.getBean(WelcomePageHandlerMapping.class);
assertThat(handler.getOrder()).isEqualTo(2);
});
}
@Test
void handlesRequestForStaticPageThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void handlesRequestForStaticPageThatAcceptsAll() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void doesNotHandleRequestThatDoesNotAcceptTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
.hasStatus(HttpStatus.NOT_FOUND)));
}
@Test
void handlesRequestWithNoAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatusOk().hasForwardedUrl("index.html")));
}
@Test
void handlesRequestWithEmptyAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, "")).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.withPropertyValues("static-path-pattern=/foo/**")
.run((context) -> assertThat(context.getBean(WelcomePageHandlerMapping.class).getRootHandler()).isNull());
}
@Test
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
this.contextRunner.run(testWith(
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
}
@Test
void handlesRequestForTemplateThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
.hasBodyTextEqualTo("index template")));
}
@Test
void handlesRequestForTemplateThatAcceptsAll() {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasBodyTextEqualTo("index template")));
}
@Test
void prefersAStaticResourceToATemplate() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class, TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void logsInvalidAcceptHeader(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept("*/*q=0.8")).hasStatusOk()
.hasBodyTextEqualTo("index template")));
assertThat(output).contains("Received invalid Accept header. Assuming all media types are accepted");
}
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
return (context) -> mvc.accept(MockMvcTester.from(context));
}
@Configuration(proxyBeanMethods = false)
static class HandlerMappingConfiguration {
@Bean
WelcomePageHandlerMapping handlerMapping(ApplicationContext applicationContext,
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
ObjectProvider<Resource> staticIndexPage,
@Value("${static-path-pattern:/**}") String staticPathPattern) {
return new WelcomePageHandlerMapping(
templateAvailabilityProviders
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
}
}
@Configuration(proxyBeanMethods = false)
static class StaticResourceConfiguration {
@Bean
Resource staticIndexPage() {
return new ByteArrayResource("welcome-page-static".getBytes(StandardCharsets.UTF_8));
}
}
@Configuration(proxyBeanMethods = false)
static class TemplateConfiguration {
@Bean
TemplateAvailabilityProviders templateAvailabilityProviders() {
return new TestTemplateAvailabilityProviders(
(view, environment, classLoader, resourceLoader) -> view.equals("index"));
}
@Bean
ViewResolver viewResolver() {
return (name, locale) -> {
if (name.startsWith("forward:")) {
return new InternalResourceView(name.substring("forward:".length()));
}
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().print(name + " template");
}
};
};
}
}
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the welcome page.
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
"spring.web.resources.chain.strategy.content.enabled=true",
"spring.thymeleaf.prefix=classpath:/org/springframework/boot/webmvc/autoconfigure/",
"spring.web.resources.static-locations=classpath:/org/springframework/boot/webmvc/autoconfigure/static" })
class WelcomePageIntegrationTests {
@LocalServerPort
private int port;
private final TestRestTemplate template = new TestRestTemplate();
@Test
void contentStrategyWithWelcomePage() throws Exception {
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
.header("Accept", MediaType.ALL.toString())
.build();
ResponseEntity<String> content = this.template.exchange(entity, String.class);
assertThat(content.getBody()).contains("/custom-");
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void notAcceptableWelcomePage() throws Exception {
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
.header("Accept", "spring/boot")
.build();
ResponseEntity<String> content = this.template.exchange(entity, String.class);
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE);
}
@Configuration
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ThymeleafAutoConfiguration.class })
static class TestConfiguration {
static void main(String[] args) {
new SpringApplicationBuilder(TestConfiguration.class).run(args);
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
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.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WelcomePageNotAcceptableHandlerMapping}.
*
* @author Phillip Webb
*/
class WelcomePageNotAcceptableHandlerMappingTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(HandlerMappingConfiguration.class)
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
@Test
void isOrderedAtLowPriorityButAboveResourceHandlerRegistry() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
WelcomePageNotAcceptableHandlerMapping handler = context
.getBean(WelcomePageNotAcceptableHandlerMapping.class);
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(context, null);
Integer resourceOrder = (Integer) ReflectionTestUtils.getField(registry, "order");
assertThat(handler.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 10);
assertThat(handler.getOrder()).isLessThan(resourceOrder);
});
}
@Test
void handlesRequestForStaticPageThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestForStaticPageThatDoesNotAcceptTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestWithNoAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestWithEmptyAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, ""))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.withPropertyValues("static-path-pattern=/foo/**")
.run((context) -> assertThat(context.getBean(WelcomePageNotAcceptableHandlerMapping.class).getRootHandler())
.isNull());
}
@Test
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
this.contextRunner.run(testWith(
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
}
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
return (context) -> mvc.accept(MockMvcTester.from(context));
}
@Configuration(proxyBeanMethods = false)
static class HandlerMappingConfiguration {
@Bean
WelcomePageNotAcceptableHandlerMapping handlerMapping(ApplicationContext applicationContext,
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
ObjectProvider<Resource> staticIndexPage,
@Value("${static-path-pattern:/**}") String staticPathPattern) {
return new WelcomePageNotAcceptableHandlerMapping(
templateAvailabilityProviders
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
}
}
@Configuration(proxyBeanMethods = false)
static class StaticResourceConfiguration {
@Bean
Resource staticIndexPage() {
return new FileSystemResource("src/test/resources/welcome-page/index.html");
}
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.servlet.ServletException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.util.ApplicationContextTestUtils;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using {@link MockMvcTester} but not
* {@link org.springframework.test.context.junit.jupiter.SpringExtension}.
*
* @author Dave Syer
* @author Sebastien Deleuze
*/
class BasicErrorControllerDirectMockMvcTests {
private ConfigurableWebApplicationContext wac;
private MockMvcTester mvc;
@AfterEach
void close() {
ApplicationContextTestUtils.closeAll(this.wac);
}
void setup(ConfigurableWebApplicationContext context) {
this.wac = context;
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void errorPageAvailableWithParentContext() {
setup((ConfigurableWebApplicationContext) new SpringApplicationBuilder(ParentConfiguration.class)
.child(ChildConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Test
void errorPageAvailableWithMvcIncluded() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Test
void errorPageNotAvailableWithWhitelabelDisabled() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
.run("--server.port=0", "--server.error.whitelabel.enabled=false"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasFailed()
.failure()
.isInstanceOf(ServletException.class);
}
@Test
void errorControllerWithAop() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WithAopConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class ParentConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
@EnableWebMvc
static class WebMvcIncludedConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(WebMvcIncludedConfiguration.class, args);
}
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class VanillaConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(VanillaConfiguration.class, args);
}
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class ChildConfiguration {
// For manual testing
static void main(String[] args) {
new SpringApplicationBuilder(ParentConfiguration.class).child(ChildConfiguration.class).run(args);
}
}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@MinimalWebConfiguration
@Aspect
static class WithAopConfiguration {
@Pointcut("within(@org.springframework.stereotype.Controller *)")
private void controllerPointCut() {
}
@Around("controllerPointCut()")
Object mvcAdvice(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
}
}

View File

@@ -0,0 +1,545 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Parameter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.freemarker.autoconfigure.FreeMarkerAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using a real HTTP server.
*
* @author Phillip Webb
* @author Dave Syer
* @author Stephane Nicoll
* @author Scott Frederick
*/
class BasicErrorControllerIntegrationTests {
private ConfigurableApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testErrorForMachineClientDefault() {
load();
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("?trace=true"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", null, null, "/");
assertThat(entity.getBody()).doesNotContainKey("exception");
assertThat(entity.getBody()).doesNotContainKey("trace");
}
@Test
void testErrorForMachineClientWithParamsTrue() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithStackTraceAndMessage("?trace=true&message=true");
}
@Test
void testErrorForMachineClientWithParamsFalse() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithoutStackTraceAndMessage("?trace=false&message=false");
}
@Test
void testErrorForMachineClientWithParamsAbsent() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithoutStackTraceAndMessage("");
}
@Test
void testErrorForMachineClientNeverParams() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=never",
"--server.error.include-message=never");
exceptionWithoutStackTraceAndMessage("?trace=true&message=true");
}
@Test
void testErrorForMachineClientAlwaysParams() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=always",
"--server.error.include-message=always");
exceptionWithStackTraceAndMessage("?trace=false&message=false");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForMachineClientAlwaysParamsWithoutMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/noMessage"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
"No message available", "/noMessage");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void exceptionWithStackTraceAndMessage(String path) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
"Expected!", "/");
assertThat(entity.getBody()).containsKey("trace");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void exceptionWithoutStackTraceAndMessage(String path) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, null, "/");
assertThat(entity.getBody()).doesNotContainKey("trace");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedExceptionWithoutMessage() {
load("--server.error.include-exception=true");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
null, "/annotated");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
"Expected!", "/annotated");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoReasonExceptionWithoutMessage() {
load("--server.error.include-exception=true");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, null, "/annotatedNoReason");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoReasonExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, "Expected message", "/annotatedNoReason");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoMessageExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoMessage"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, "No message available",
"/annotatedNoMessage");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamTrue() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithErrors("?errors=true");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamFalse() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithoutErrors("?errors=false");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamAbsent() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithoutErrors("");
}
@Test
void testBindingExceptionForMachineClientAlwaysErrors() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=always");
bindingExceptionWithErrors("?errors=false");
}
@Test
void testBindingExceptionForMachineClientNeverErrors() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=never");
bindingExceptionWithoutErrors("?errors=true");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamTrue() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithMessage("?message=true");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamFalse() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithoutMessage("?message=false");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamAbsent() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithoutMessage("");
}
@Test
void testBindingExceptionForMachineClientAlwaysMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
bindingExceptionWithMessage("?message=false");
}
@Test
void testBindingExceptionForMachineClientNeverMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=never");
bindingExceptionWithoutMessage("?message=true");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithErrors(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).containsKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithoutErrors(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithMessage(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class,
"Validation failed for object='test'. Error count: 1", "/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithoutMessage(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testRequestBodyValidationForMachineClient() {
load("--server.error.include-exception=true");
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bodyValidation");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testBindingExceptionForMachineClientDefault() {
load();
RequestEntity request = RequestEntity.get(URI.create(createUrl("/bind?trace=true,message=true")))
.accept(MediaType.APPLICATION_JSON)
.build();
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertThat(entity.getBody()).doesNotContainKey("exception");
assertThat(entity.getBody()).doesNotContainKey("trace");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@WithResource(name = "templates/error/507.ftlh", content = "We are out of storage")
void testConventionTemplateMapping() {
load();
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/noStorage")))
.accept(MediaType.TEXT_HTML)
.build();
ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class);
String resp = entity.getBody();
assertThat(resp).contains("We are out of storage");
}
@Test
void testIncompatibleMediaType() {
load();
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/incompatibleType")))
.accept(MediaType.TEXT_PLAIN)
.build();
ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(entity.getHeaders().getContentType()).isNull();
assertThat(entity.getBody()).isNull();
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void customErrorControllerWithoutStatusConfiguration() {
load(CustomErrorControllerWithoutStatusConfiguration.class);
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertThat(entity.getBody()).doesNotContainKey("status");
}
private void assertErrorAttributes(Map<?, ?> content, String status, String error, Class<?> exception,
String message, String path) {
assertThat(content.get("status")).as("Wrong status").hasToString(status);
assertThat(content.get("error")).as("Wrong error").isEqualTo(error);
if (exception != null) {
assertThat(content.get("exception")).as("Wrong exception").isEqualTo(exception.getName());
}
else {
assertThat(content.containsKey("exception")).as("Exception attribute should not be set").isFalse();
}
assertThat(content.get("message")).as("Wrong message").isEqualTo(message);
assertThat(content.get("path")).as("Wrong path").isEqualTo(path);
}
private String createUrl(String path) {
int port = this.context.getEnvironment().getProperty("local.server.port", int.class);
return "http://localhost:" + port + path;
}
private void load(String... arguments) {
load(TestConfiguration.class, arguments);
}
private void load(Class<?> configuration, String... arguments) {
List<String> args = new ArrayList<>();
args.add("--server.port=0");
if (arguments != null) {
args.addAll(Arrays.asList(arguments));
}
this.context = SpringApplication.run(configuration, StringUtils.toStringArray(args));
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
private @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
public static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
@Bean
View error() {
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().write("ERROR_BEAN");
}
};
}
@RestController
public static class Errors {
public String getFoo() {
return "foo";
}
@RequestMapping("/")
String home() {
throw new IllegalStateException("Expected!");
}
@RequestMapping("/noMessage")
String noMessage() {
throw new IllegalStateException();
}
@RequestMapping("/annotated")
String annotated() {
throw new ExpectedException();
}
@RequestMapping("/annotatedNoReason")
String annotatedNoReason() {
throw new NoReasonExpectedException("Expected message");
}
@RequestMapping("/annotatedNoMessage")
String annotatedNoMessage() {
throw new NoReasonExpectedException("");
}
@RequestMapping("/bind")
String bind(@RequestAttribute(required = false) String foo) throws Exception {
BindException error = new BindException(this, "test");
error.rejectValue("foo", "bar.error");
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
.getParameters()[0];
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
}
@PostMapping(path = "/bodyValidation", produces = "application/json")
String bodyValidation(@Valid @RequestBody DummyBody body) {
return body.content;
}
@RequestMapping(path = "/noStorage")
String noStorage() {
throw new InsufficientStorageException();
}
@RequestMapping(path = "/incompatibleType", produces = "text/plain")
String incompatibleType() {
throw new ExpectedException();
}
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
@SuppressWarnings("serial")
static class ExpectedException extends RuntimeException {
}
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
static class InsufficientStorageException extends RuntimeException {
}
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@SuppressWarnings("serial")
static class NoReasonExpectedException extends RuntimeException {
NoReasonExpectedException(String message) {
super(message);
}
}
static class DummyBody {
@NotNull
private String content;
String getContent() {
return this.content;
}
void setContent(String content) {
this.content = content;
}
}
}
}
static class CustomErrorControllerWithoutStatusConfiguration extends TestConfiguration {
@Bean
BasicErrorController basicErrorController(ServerProperties serverProperties, ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, serverProperties.getError(),
errorViewResolvers.orderedStream().toList()) {
@Override
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request,
MediaType mediaType) {
return super.getErrorAttributeOptions(request, mediaType).excluding(Include.STATUS);
}
};
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Parameter;
import java.util.Map;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using {@link MockMvcTester} and
* {@link SpringBootTest @SpringBootTest}.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest(properties = { "server.error.include-message=always", "debug=true" })
@DirtiesContext
class BasicErrorControllerMockMvcTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void testDirectAccessForMachineClient() {
assertThat(this.mvc.get().uri("/error")).hasStatus5xxServerError().bodyText().contains("999");
}
@Test
void testErrorWithNotFoundResponseStatus() {
assertThat(this.mvc.get().uri("/bang")).hasStatus(HttpStatus.NOT_FOUND)
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
.contains("Expected!"));
}
@Test
void testErrorWithNoContentResponseStatus() {
assertThat(this.mvc.get().uri("/noContent").accept("some/thing")).hasStatus(HttpStatus.NO_CONTENT)
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error")))
.hasStatus(HttpStatus.NO_CONTENT)
.body()
.isEmpty());
}
@Test
void testBindingExceptionForMachineClient() {
// In a real server the response is carried over into the error dispatcher, but
// in the mock a new one is created, so we have to assert the status at this
// intermediate point, and the rendered status code is always wrong (but would
// be 400 in a real system)
assertThat(this.mvc.get().uri("/bind")).hasStatus4xxClientError()
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
.contains("Validation failed"));
}
@Test
void testDirectAccessForBrowserClient() {
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("ERROR_BEAN");
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
private @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
@Bean
View error() {
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().write("ERROR_BEAN");
}
};
}
@RestController
public static class Errors {
@RequestMapping("/")
String home() {
throw new IllegalStateException("Expected!");
}
@RequestMapping("/bang")
String bang() {
throw new NotFoundException("Expected!");
}
@RequestMapping("/bind")
String bind(@RequestAttribute(required = false) String foo) throws Exception {
BindException error = new BindException(this, "test");
error.rejectValue("foo", "bar.error");
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
.getParameters()[0];
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
}
@RequestMapping("/noContent")
void noContent() {
throw new NoContentException("Expected!");
}
public String getFoo() {
return "foo";
}
}
}
@ResponseStatus(HttpStatus.NOT_FOUND)
static class NotFoundException extends RuntimeException {
NotFoundException(String string) {
super(string);
}
}
@ResponseStatus(HttpStatus.NO_CONTENT)
private static class NoContentException extends RuntimeException {
NoContentException(String string) {
super(string);
}
}
private class ErrorDispatcher implements RequestBuilder {
private final MvcResult result;
private final String path;
ErrorDispatcher(MvcTestResult mvcTestResult, String path) {
this.result = mvcTestResult.getMvcResult();
this.path = path;
}
@Override
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
MockHttpServletRequest request = this.result.getRequest();
request.setDispatcherType(DispatcherType.ERROR);
request.setRequestURI(this.path);
request.setAttribute("jakarta.servlet.error.status_code", this.result.getResponse().getStatus());
return request;
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the default error view.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest(properties = { "server.error.include-message=always" })
@DirtiesContext
class DefaultErrorViewIntegrationTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void testErrorForBrowserClient() {
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("<html>", "999");
}
@Test
void testErrorWithHtmlEscape() {
assertThat(this.mvc.get()
.uri("/error")
.requestAttr("jakarta.servlet.error.exception",
new RuntimeException("<script>alert('Hello World')</script>"))
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("&lt;script&gt;", "Hello World", "999");
}
@Test
void testErrorWithSpelEscape() {
String spel = "${T(" + getClass().getName() + ").injectCall()}";
assertThat(this.mvc.get()
.uri("/error")
.requestAttr("jakarta.servlet.error.exception", new RuntimeException(spel))
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError().bodyText().doesNotContain("injection");
}
static String injectCall() {
return "injection";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultErrorViewResolver}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
@ExtendWith(MockitoExtension.class)
class DefaultErrorViewResolverTests {
private DefaultErrorViewResolver resolver;
@Mock
private TemplateAvailabilityProvider templateAvailabilityProvider;
private Resources resourcesProperties;
private final Map<String, Object> model = new HashMap<>();
private final HttpServletRequest request = new MockHttpServletRequest();
@BeforeEach
void setup() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
this.resourcesProperties = new Resources();
TemplateAvailabilityProviders templateAvailabilityProviders = new TestTemplateAvailabilityProviders(
this.templateAvailabilityProvider);
this.resolver = new DefaultErrorViewResolver(applicationContext, this.resourcesProperties,
templateAvailabilityProviders);
}
@Test
void createWhenApplicationContextIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultErrorViewResolver(null, new Resources()))
.withMessageContaining("'applicationContext' must not be null");
}
@Test
void createWhenResourcePropertiesIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DefaultErrorViewResolver(mock(ApplicationContext.class), (Resources) null))
.withMessageContaining("'resources' must not be null");
}
@Test
void resolveWhenNoMatchShouldReturnNull() {
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNull();
}
@Test
void resolveWhenExactTemplateMatchShouldReturnTemplate() {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNotNull();
assertThat(resolved.getViewName()).isEqualTo("error/404");
then(this.templateAvailabilityProvider).should()
.isTemplateAvailable(eq("error/404"), any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class));
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
}
@Test
void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/503"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/5xx"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.SERVICE_UNAVAILABLE,
this.model);
assertThat(resolved.getViewName()).isEqualTo("error/5xx");
}
@Test
void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
assertThat(resolved.getViewName()).isEqualTo("error/4xx");
}
@Test
void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/exact");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/4xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("4xx/4xx");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/5xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.INTERNAL_SERVER_ERROR,
this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("5xx/5xx");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenTemplateAndResourceMatchShouldFavorTemplate() {
setResourceLocation("/exact");
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(true);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
assertThat(resolved.getViewName()).isEqualTo("error/404");
}
@Test
void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource() throws Exception {
setResourceLocation("/exact");
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void orderShouldBeLowest() {
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
}
@Test
void setOrderShouldChangeOrder() {
this.resolver.setOrder(123);
assertThat(this.resolver.getOrder()).isEqualTo(123);
}
private void setResourceLocation(String path) {
String packageName = getClass().getPackage().getName();
this.resourcesProperties
.setStaticLocations(new String[] { "classpath:" + packageName.replace('.', '/') + path + "/" });
}
private MockHttpServletResponse render(ModelAndView modelAndView) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
modelAndView.getView().render(this.model, this.request, response);
return response;
}
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import java.time.Clock;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.handler.DispatcherServletWebRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ErrorMvcAutoConfiguration}.
*
* @author Brian Clozel
* @author Scott Frederick
*/
@ExtendWith(OutputCaptureExtension.class)
class ErrorMvcAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration(
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class));
@Test
void renderContainsViewWithExceptionDetails() {
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
false);
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
webRequest.getResponse());
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
assertThat(responseString).contains(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.contains("<div>Exception message</div>")
.contains("<div style='white-space:pre-wrap;'>java.lang.IllegalStateException");
});
}
@Test
void renderCanUseJavaTimeTypeAsTimestamp() { // gh-23256
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
false);
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest, withAllOptions());
attributes.put("timestamp", Clock.systemUTC().instant());
errorView.render(attributes, webRequest.getRequest(), webRequest.getResponse());
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
assertThat(responseString).contains("This application has no explicit mapping for /error");
});
}
@Test
void renderWhenAlreadyCommittedLogsMessage(CapturedOutput output) {
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
true);
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
webRequest.getResponse());
assertThat(output).contains("Cannot render error page for request [/path] "
+ "and exception [Exception message] as the response has "
+ "already been committed. As a result, the response may have the wrong status code.");
});
}
private DispatcherServletWebRequest createWebRequest(Exception ex, boolean committed) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
MockHttpServletResponse response = new MockHttpServletResponse();
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
webRequest.setAttribute("jakarta.servlet.error.exception", ex, RequestAttributes.SCOPE_REQUEST);
webRequest.setAttribute("jakarta.servlet.error.request_uri", "/path", RequestAttributes.SCOPE_REQUEST);
response.setCommitted(committed);
response.setOutputStreamAccessAllowed(!committed);
response.setWriterAccessAllowed(!committed);
return webRequest;
}
private ErrorAttributeOptions withAllOptions() {
return ErrorAttributeOptions.of(Include.values());
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.autoconfigure.error;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Controller;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for remapped error pages.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.mvc.servlet.path:/spring/")
@DirtiesContext
class RemappedErrorViewIntegrationTests {
@LocalServerPort
private int port;
private final TestRestTemplate template = new TestRestTemplate();
@Test
void directAccessToErrorPage() {
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/error", String.class);
assertThat(content).contains("error");
assertThat(content).contains("999");
}
@Test
void forwardToErrorPage() {
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/", String.class);
assertThat(content).contains("error");
assertThat(content).contains("500");
}
@Configuration(proxyBeanMethods = false)
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class })
@Controller
static class TestConfiguration implements ErrorPageRegistrar {
@RequestMapping("/")
String home() {
throw new RuntimeException("Planned!");
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
errorPageRegistry.addErrorPages(new ErrorPage("/spring/error"));
}
// For manual testing
static void main(String[] args) {
new SpringApplicationBuilder(TestConfiguration.class).properties("spring.mvc.servlet.path:spring/*")
.run(args);
}
}
}

View File

@@ -0,0 +1,333 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.error;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.servlet.ModelAndView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultErrorAttributes}.
*
* @author Phillip Webb
* @author Vedran Pavic
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
*/
class DefaultErrorAttributesTests {
private final DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final WebRequest webRequest = new ServletWebRequest(this.request);
@Test
void includeTimeStamp() {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes.get("timestamp")).isInstanceOf(Date.class);
}
@Test
void specificStatusCode() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("error", HttpStatus.NOT_FOUND.getReasonPhrase());
assertThat(attributes).containsEntry("status", 404);
}
@Test
void missingStatusCode() {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("error", "None");
assertThat(attributes).containsEntry("status", 999);
}
@Test
void mvcError() {
RuntimeException ex = new RuntimeException("Test");
ModelAndView modelAndView = this.errorAttributes.resolveException(this.request, null, null, ex);
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException("Ignored"));
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(modelAndView).isNull();
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletErrorWithMessage() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletErrorWithoutMessage() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).doesNotContainKey("message");
}
@Test
void servletMessageWithMessage() {
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletMessageWithoutMessage() {
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).doesNotContainKey("message");
}
@Test
void nullExceptionMessage() {
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void nullExceptionMessageAndServletMessage() {
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "No message available");
}
@Test
void unwrapServletException() {
RuntimeException ex = new RuntimeException("Test");
ServletException wrapped = new ServletException(new ServletException(ex));
this.request.setAttribute("jakarta.servlet.error.exception", wrapped);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(wrapped);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void getError() {
Error error = new OutOfMemoryError("Test error");
this.request.setAttribute("jakarta.servlet.error.exception", error);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(error);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test error");
}
@Test
void withBindingErrors() {
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new BindException(bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
@Test
void withoutBindingErrors() {
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new BindException(bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.defaults());
}
@Test
void withMethodArgumentNotValidExceptionBindingErrors() {
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
MethodParameter parameter = new MethodParameter(method, 0);
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new MethodArgumentNotValidException(parameter, bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
@Test
void withHandlerMethodValidationExceptionBindingErrors() {
Object target = "test";
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
MethodParameter parameter = new MethodParameter(method, 0);
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
List.of(new ParameterValidationResult(parameter, -1,
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
(error, sourceType) -> {
throw new IllegalArgumentException("No source object of the given type");
})));
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
testErrors(methodValidationResult.getAllErrors(),
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1",
ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
private void testBindingResult(BindingResult bindingResult, Exception ex, ErrorAttributeOptions options) {
testErrors(bindingResult.getAllErrors(), "Validation failed for object='objectName'. Error count: 1", ex,
options);
}
private void testErrors(List<? extends MessageSourceResolvable> errors, String expectedMessage, Exception ex,
ErrorAttributeOptions options) {
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest, options);
if (options.isIncluded(Include.MESSAGE)) {
assertThat(attributes).containsEntry("message", expectedMessage);
}
else {
assertThat(attributes).doesNotContainKey("message");
}
if (options.isIncluded(Include.BINDING_ERRORS)) {
assertThat(attributes).containsEntry("errors", org.springframework.boot.web.error.Error.wrap(errors));
}
else {
assertThat(attributes).doesNotContainKey("errors");
}
}
@Test
void withExceptionAttribute() {
DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
assertThat(attributes).containsEntry("exception", RuntimeException.class.getName());
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void withStackTraceAttribute() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.STACK_TRACE));
assertThat(attributes.get("trace").toString()).startsWith("java.lang");
}
@Test
void withoutStackTraceAttribute() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).doesNotContainKey("trace");
}
@Test
void shouldIncludePathByDefault() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldIncludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.PATH));
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldExcludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of());
assertThat(attributes).doesNotContainEntry("path", "path");
}
@Test
void whenGetMessageIsOverriddenThenMessageAttributeContainsValueReturnedFromIt() {
Map<String, Object> attributes = new DefaultErrorAttributes() {
@Override
protected String getMessage(WebRequest webRequest, Throwable error) {
return "custom message";
}
}.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).containsEntry("message", "custom message");
}
@Test
void excludeStatus() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults().excluding(Include.STATUS));
assertThat(attributes).doesNotContainKey("status");
}
@Test
void excludeError() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults().excluding(Include.ERROR));
assertThat(attributes).doesNotContainKey("error");
}
}

View File

@@ -0,0 +1,10 @@
<!doctype html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Test Thymeleaf</title>
<link th:href="@{/custom.css}" rel="stylesheet" />
</head>
<body>
</body>
</html>