Commit 0bf025af authored by Phillip Webb's avatar Phillip Webb

Add out-of-the-box support for status error pages

Allow convention based status error pages. Static HTML or templates
can be used by placing the appropriately named file under a `/error`
folder. For example:

	/src/main/resource/templates/error/404.ftl
or	/src/main/resource/public/error/404.html

Pages can also be named after the status series (5xx or 4xx).

Fixes gh-2691
parent 0bd246a3
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2016 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.
......@@ -16,15 +16,20 @@
package org.springframework.boot.autoconfigure.web;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
/**
* Abstract base class for error {@link Controller} implementations.
......@@ -38,9 +43,27 @@ public abstract class AbstractErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
private final List<ErrorViewResolver> errorViewResolvers;
public AbstractErrorController(ErrorAttributes errorAttributes) {
this(errorAttributes, null);
}
public AbstractErrorController(ErrorAttributes errorAttributes,
List<ErrorViewResolver> errorViewResolvers) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
}
private List<ErrorViewResolver> sortErrorViewResolvers(
List<ErrorViewResolver> resolvers) {
List<ErrorViewResolver> sorted = new ArrayList<ErrorViewResolver>();
if (resolvers != null) {
sorted.addAll(resolvers);
AnnotationAwareOrderComparator.sortIfNecessary(sorted);
}
return sorted;
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
......@@ -72,4 +95,26 @@ public abstract class AbstractErrorController implements ErrorController {
}
}
/**
* Resolve any specific error views. By default this method delegates to
* {@link ErrorViewResolver ErrorViewResolvers}.
* @param request the request
* @param response the response
* @param status the HTTP status
* @param model the suggested model
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
* @since 1.4.0
*/
protected ModelAndView resolveErrorView(HttpServletRequest request,
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
}
......@@ -16,6 +16,8 @@
package org.springframework.boot.autoconfigure.web;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
......@@ -58,7 +60,19 @@ public class BasicErrorController extends AbstractErrorController {
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
super(errorAttributes);
this(errorAttributes, errorProperties,
Collections.<ErrorViewResolver>emptyList());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
* @param errorViewResolvers error view resolvers
*/
public BasicErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
......@@ -71,10 +85,12 @@ public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map<String, Object> model = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML));
return new ModelAndView("error", model);
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
}
@RequestMapping
......
/*
* Copyright 2012-2016 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
*
* http://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.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatus.Series;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
/**
* Default {@link ErrorViewResolver} implementation that attempts to resolve error views
* using well known conventions. Will search for templates and static assets under
* {@code '/error'} using the {@link HttpStatus status code} and the
* {@link HttpStatus#series() status series}.
* <p>
* For example, an {@code HTTP 404} will search (in the specific order):
* <ul>
* <li>{@code '/<templates>/error/404.<ext>'}</li>
* <li>{@code '/<static>/error/404.html'}</li>
* <li>{@code '/<templates>/error/4xx.<ext>'}</li>
* <li>{@code '/<static>/error/4xx.html'}</li>
* </ul>
*
* @author Phillip Webb
* @since 1.4.0
*/
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new HashMap<Series, String>();
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<TemplateAvailabilityProvider> templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this(applicationContext, resourceProperties,
loadTemplateAvailabilityProviders(applicationContext));
}
private static List<TemplateAvailabilityProvider> loadTemplateAvailabilityProviders(
ApplicationContext applicationContext) {
return SpringFactoriesLoader.loadFactories(TemplateAvailabilityProvider.class,
applicationContext == null ? null : applicationContext.getClassLoader());
}
DefaultErrorViewResolver(ApplicationContext applicationContext,
ResourceProperties resourceProperties,
List<TemplateAvailabilityProvider> templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
ModelAndView modelAndView = resolveTemplate(viewName, model);
if (modelAndView == null) {
modelAndView = resolveResource(viewName, model);
}
return modelAndView;
}
private ModelAndView resolveTemplate(String viewName, Map<String, Object> model) {
for (TemplateAvailabilityProvider templateAvailabilityProvider : this.templateAvailabilityProviders) {
if (templateAvailabilityProvider.isTemplateAvailable("error/" + viewName,
this.applicationContext.getEnvironment(),
this.applicationContext.getClassLoader(), this.applicationContext)) {
return new ModelAndView("error/" + viewName, model);
}
}
return null;
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative("error/" + viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@link View} backed by a HTML resource.
*/
private static class HtmlResourceView implements View {
private Resource resource;
HtmlResourceView(Resource resource) {
this.resource = resource;
}
@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
FileCopyUtils.copy(this.resource.getInputStream(),
response.getOutputStream());
}
}
}
......@@ -27,11 +27,13 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
......@@ -40,9 +42,11 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ErrorPage;
import org.springframework.boot.web.servlet.ErrorPageRegistrar;
import org.springframework.boot.web.servlet.ErrorPageRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
......@@ -69,18 +73,28 @@ import org.springframework.web.util.HtmlUtils;
* @author Andy Wilkinson
* @author Stephane Nicoll
*/
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@Configuration
@ConditionalOnWebApplication
// Ensure this loads before the main WebMvcAutoConfiguration so that the error View is
// available
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@Configuration
@EnableConfigurationProperties(ResourceProperties.class)
public class ErrorMvcAutoConfiguration {
private final ServerProperties properties;
private final ApplicationContext applicationContext;
private final ServerProperties serverProperties;
public ErrorMvcAutoConfiguration(ServerProperties properties) {
this.properties = properties;
private final ResourceProperties resourceProperties;
@Autowired(required = false)
private List<ErrorViewResolver> errorViewResolvers;
public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
ServerProperties serverProperties, ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.serverProperties = serverProperties;
this.resourceProperties = resourceProperties;
}
@Bean
......@@ -92,12 +106,21 @@ public class ErrorMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(errorAttributes, this.properties.getError());
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
this.errorViewResolvers);
}
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.properties);
return new ErrorPageCustomizer(this.serverProperties);
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean
public DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext,
this.resourceProperties);
}
@Bean
......
/*
* Copyright 2012-2016 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
*
* http://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.autoconfigure.web;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
/**
* Interface that can be implemented by beans that resolve error views.
*
* @author Phillip Webb
* @since 1.4.0
*/
public interface ErrorViewResolver {
/**
* Resolve an error view for the specified details.
* @param request the source request
* @param status the http status of the error
* @param model the suggested model to be used with the view
* @return a resolved {@link ModelAndView} or {@code null}
*/
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
Map<String, Object> model);
}
......@@ -32,6 +32,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.MinimalWebConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
......@@ -39,6 +40,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
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;
......@@ -173,6 +175,17 @@ public class BasicErrorControllerIntegrationTests {
assertThat(resp).contains(MethodArgumentNotValidException.class.getName());
}
@Test
public void testConventionTemplateMapping() throws Exception {
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().toString();
assertThat(resp).contains("We are out of storage");
}
private void assertErrorAttributes(Map<?, ?> content, String status, String error,
Class<?> exception, String message, String path) {
assertThat(content.get("status")).as("Wrong status").isEqualTo(status);
......@@ -201,6 +214,7 @@ public class BasicErrorControllerIntegrationTests {
@Configuration
@MinimalWebConfiguration
@Import(FreeMarkerAutoConfiguration.class)
public static class TestConfiguration {
// For manual testing
......@@ -254,12 +268,22 @@ public class BasicErrorControllerIntegrationTests {
return body.content;
}
@RequestMapping(path = "/noStorage")
public String noStorage() {
throw new InsufficientStorageException();
}
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
@SuppressWarnings("serial")
private static class ExpectedException extends RuntimeException {
}
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
private static class InsufficientStorageException extends RuntimeException {
}
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@SuppressWarnings("serial")
private static class NoReasonExpectedException extends RuntimeException {
......
/*
* Copyright 2012-2016 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
*
* http://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.autoconfigure.web;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
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.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.mockito.BDDMockito.given;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link DefaultErrorViewResolver}.
*
* @author Phillip Webb
*/
public class DefaultErrorViewResolverTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private DefaultErrorViewResolver resolver;
@Mock
private TemplateAvailabilityProvider templateAvailabilityProvider;
private ResourceProperties resourceProperties;
private Map<String, Object> model = new HashMap<String, Object>();
private HttpServletRequest request = new MockHttpServletRequest();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
this.resourceProperties = new ResourceProperties();
List<TemplateAvailabilityProvider> templateAvailabilityProviders = Collections
.singletonList(this.templateAvailabilityProvider);
this.resolver = new DefaultErrorViewResolver(applicationContext,
this.resourceProperties, templateAvailabilityProviders);
}
@Test
public void createWhenApplicationContextIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ApplicationContext must not be null");
new DefaultErrorViewResolver(null, new ResourceProperties());
}
@Test
public void createWhenResourcePropertiesIsNullShouldThrowException()
throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("ResourceProperties must not be null");
new DefaultErrorViewResolver(mock(ApplicationContext.class), null);
}
@Test
public void resolveWhenNoMatchShouldReturnNull() throws Exception {
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNull();
}
@Test
public void resolveWhenExactTemplateMatchShouldReturnTemplate() throws Exception {
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");
verify(this.templateAvailabilityProvider).isTemplateAvailable(eq("error/404"),
any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class));
verifyNoMoreInteractions(this.templateAvailabilityProvider);
}
@Test
public void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() throws Exception {
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
public void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() throws Exception {
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
public void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/exact");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(render(resolved)).isEqualTo("exact/404");
}
@Test
public void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/4xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.NOT_FOUND, this.model);
assertThat(render(resolved)).isEqualTo("4xx/4xx");
}
@Test
public void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/5xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
HttpStatus.INTERNAL_SERVER_ERROR, this.model);
assertThat(render(resolved)).isEqualTo("5xx/5xx");
}
@Test
public void resolveWhenTemplateAndResourceMatchShouldFavorTemplate()
throws Exception {
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
public void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource()
throws Exception {
setResourceLocation("/exact");
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(render(resolved)).isEqualTo("exact/404");
}
@Test
public void orderShouldBeLowest() throws Exception {
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
}
@Test
public void setOrderShouldChangeOrder() throws Exception {
this.resolver.setOrder(123);
assertThat(this.resolver.getOrder()).isEqualTo(123);
}
private void setResourceLocation(String path) {
String packageName = getClass().getPackage().getName();
this.resourceProperties.setStaticLocations(new String[] {
"classpath:" + packageName.replace(".", "/") + path + "/" });
}
private String render(ModelAndView modelAndView) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
modelAndView.getView().render(this.model, this.request, response);
return response.getContentAsString().trim();
}
}
......@@ -1713,34 +1713,98 @@ In the example above, if `YourException` is thrown by a controller defined in th
package as `FooController`, a json representation of the `CustomerErrorType` POJO will be
used instead of the `ErrorAttributes` representation.
If you want more specific error pages for some conditions, the embedded servlet containers
support a uniform Java DSL for customizing the error handling. Assuming that you have a
mapping for `/400`:
[source,java,indent=0,subs="verbatim,quotes,attributes"]
[[boot-features-error-handling-custom-error-pages]]
===== Custom error pages
If you want to display a custom HTML error page for a given status code, you add a file to
an `/error` folder. Error pages can either be static HTML (i.e. added under any of the
static resource folders) or built using templates. The name of the file should be the
exact status code or a series mask.
For example, to map `404` to a static HTML file, your folder structure would look like
this:
[source,indent=0,subs="verbatim,quotes,attributes"]
----
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
----
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer(){
return new MyCustomizer();
}
// ...
To map all `5xx` errors using a freemarker template, you'd have a structure like this:
private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
[source,indent=0,subs="verbatim,quotes,attributes"]
----
src/
+- main/
+- java/
| + <source code>
+- resources/
+- template/
+- error/
| +- 5xx.ftl
+- <other templates>
----
For more complex mappings you can also add beans that implement the `ErrorViewResolver`
interface.
[source,java,indent=0,subs="verbatim,quotes,attributes"]
----
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
public ModelAndView resolveErrorView(HttpServletRequest request,
HttpStatus status, Map<String, Object> model) {
// Use the request or status to optionally return a ModelAndView
return ...
}
}
----
You can also use regular Spring MVC features like
{spring-reference}/#mvc-exceptionhandlers[`@ExceptionHandler` methods] and
{spring-reference}/#mvc-ann-controller-advice[`@ControllerAdvice`]. The `ErrorController`
will then pick up any unhandled exceptions.
[[boot-features-error-handling-mapping-error-pages-without-mvc]]
===== Mapping error pages outside of Spring MVC
For applications that aren't using Spring MVC, you can use the `ErrorPageRegistrar`
interface to directly register `ErrorPages`. This abstraction works directly with the
underlying embedded servlet container and will work even if you don't have a Spring MVC
`DispatcherServlet`
[source,java,indent=0,subs="verbatim,quotes,attributes"]
----
@Bean
public ErrorPageRegistrar errorPageRegistrar(){
return new MyErrorPageRegistrar();
}
// ...
private static class MyErrorPageRegistrar implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
}
}
----
N.B. if you register an `ErrorPage` with a path that will end up being handled by a
`Filter` (e.g. as is common with some non-Spring web frameworks, like Jersey and Wicket),
then the `Filter` has to be explicitly registered as an `ERROR` dispatcher, e.g.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment