Support error conventions in Spring WebFlux

This commit adds support for Spring Boot error conventions with WebFlux.
The Spring MVC support for that is based on an `Controller` that's
mapped on a specific `"/error"` path and configured as an error page in
the Servlet container. With WebFlux, this support leverages a
`WebExceptionHandler`, which catches exceptions flowing through the
reactive pipeline and handles them.

The `DefaultErrorWebExceptionHandler` supports the following:
* return a JSON error response to machine clients
* return error HTML views (templates, static or default HTML view)

One can customize the error information by contributing an
`ErrorAttributes` bean to the application context.

Spring Boot provides an `ErrorWebExceptionHandler` marker interface and a
base implementation that provides high level constructs to handle
errors, based on the Spring WebFlux functional flavor.
The error handling logic can be completely changed by providing a custom
`RouterFunction` there.

Fixes gh-8625
pull/10616/merge
Brian Clozel 7 years ago
parent c4adb76df2
commit 68db43cf44

@ -127,7 +127,7 @@ public class WebFluxAutoConfiguration {
@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
if (this.argumentResolvers != null) {
this.argumentResolvers.stream().forEach(configurer::addCustomResolver);
this.argumentResolvers.forEach(configurer::addCustomResolver);
}
}

@ -0,0 +1,224 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
/**
* Abstract base class for {@link ErrorWebExceptionHandler} implementations.
*
* @author Brian Clozel
* @since 2.0.0
* @see ErrorAttributes
*/
public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExceptionHandler, InitializingBean {
private final ApplicationContext applicationContext;
private final ErrorAttributes errorAttributes;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
private List<ViewResolver> viewResolvers = Collections.emptyList();
public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties,
ApplicationContext applicationContext) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
Assert.notNull(applicationContext, "ApplicationContext must not be null");
this.errorAttributes = errorAttributes;
this.resourceProperties = resourceProperties;
this.applicationContext = applicationContext;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
/**
* Configure HTTP message writers to serialize the response body with.
* @param messageWriters the {@link HttpMessageWriter}s to use
*/
public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
Assert.notNull(messageWriters, "'messageWriters' must not be null");
this.messageWriters = messageWriters;
}
/**
* Configure HTTP message readers to deserialize the request body with.
* @param messageReaders the {@link HttpMessageReader}s to use
*/
public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
Assert.notNull(messageReaders, "'messageReaders' must not be null");
this.messageReaders = messageReaders;
}
/**
* Configure the {@link ViewResolver} to use for rendering views.
* @param viewResolvers the list of {@link ViewResolver}s to use
*/
public void setViewResolvers(List<ViewResolver> viewResolvers) {
this.viewResolvers = viewResolvers;
}
/**
* Extract the error attributes from the current request, to be used
* to populate error views or JSON payloads.
* @param request the source request
* @param includeStackTrace whether to include the error stacktrace information
* @return the error attributes as a Map.
*/
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
return this.errorAttributes.getErrorAttributes(request, includeStackTrace);
}
protected boolean isTraceEnabled(ServerRequest request) {
String parameter = request.queryParam("trace").orElse("false");
return !"false".equals(parameter.toLowerCase());
}
/**
* Render the given error data as a view, using a template view if available
* or a static HTML file if available otherwise. This will return an empty
* {@code Publisher} if none of the above are available.
* @param viewName the view name
* @param responseBody the error response being built
* @param error the error data as a map
* @return a Publisher of the {@link ServerResponse}
*/
protected Mono<ServerResponse> renderErrorView(String viewName, ServerResponse.BodyBuilder responseBody,
Map<String, Object> error) {
if (isTemplateAvailable(viewName)) {
return responseBody.render(viewName, error);
}
Resource resource = resolveResource(viewName);
if (resource != null) {
return responseBody.body(BodyInserters.fromResource(resource));
}
return Mono.empty();
}
private boolean isTemplateAvailable(String viewName) {
return this.templateAvailabilityProviders.getProvider(viewName, this.applicationContext) != null;
}
private Resource resolveResource(String viewName) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return resource;
}
}
catch (Exception ex) {
}
}
return null;
}
/**
* Render a default HTML "Whitelabel Error Page".
* <p>Useful when no other error view is available in the application.
* @param responseBody the error response being built
* @param error the error data as a map
* @return a Publisher of the {@link ServerResponse}
*/
protected Mono<ServerResponse> renderDefaultErrorView(ServerResponse.BodyBuilder responseBody,
Map<String, Object> error) {
StringBuilder builder = new StringBuilder();
Date timestamp = (Date) error.get("timestamp");
builder.append("<html><body><h1>Whitelabel Error Page</h1>")
.append("<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
.append("<div id='created'>").append(timestamp.toString()).append("</div>")
.append("<div>There was an unexpected error (type=")
.append(error.get("error")).append(", status=").append(error.get("status"))
.append(").</div>")
.append("<div>").append(error.get("message")).append("</div>")
.append("</body></html>");
return responseBody.syncBody(builder.toString());
}
@Override
public void afterPropertiesSet() throws Exception {
if (CollectionUtils.isEmpty(this.messageWriters)) {
throw new IllegalArgumentException("Property 'messageWriters' is required");
}
}
/**
* Create a {@link RouterFunction} that can route and handle errors as JSON responses
* or HTML views.
* <p>If the returned {@link RouterFunction} doesn't route to a {@code HandlerFunction},
* the original exception is propagated in the pipeline and can be processed by other
* {@link org.springframework.web.server.WebExceptionHandler}s.
* @param errorAttributes the {@code ErrorAttributes} instance to use to extract error information
* @return a {@link RouterFunction} that routes and handles errors
*/
protected abstract RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
this.errorAttributes.storeErrorInformation(throwable, exchange);
ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
return getRoutingFunction(this.errorAttributes)
.route(request)
.switchIfEmpty(Mono.error(throwable))
.flatMap(handler -> handler.handle(request))
.flatMap(response -> {
// force content-type since writeTo won't overwrite response header values
exchange.getResponse().getHeaders().setContentType(response.headers().getContentType());
return response.writeTo(exchange, new ServerResponse.Context() {
@Override
public List<HttpMessageWriter<?>> messageWriters() {
return AbstractErrorWebExceptionHandler.this.messageWriters;
}
@Override
public List<ViewResolver> viewResolvers() {
return AbstractErrorWebExceptionHandler.this.viewResolvers;
}
});
});
}
}

@ -0,0 +1,127 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
/**
* Default implementation of {@link ErrorAttributes}. Provides the following attributes
* when possible:
* <ul>
* <li>timestamp - The time that the errors were extracted</li>
* <li>status - The status code</li>
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception (if configured)</li>
* <li>message - The exception message</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception
* <li>trace - The exception stack trace</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
*
* @author Brian Clozel
* @since 2.0.0
* @see ErrorAttributes
*/
public class DefaultErrorAttributes implements ErrorAttributes {
private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName()
+ ".ERROR";
private final boolean includeException;
/**
* Create a new {@link DefaultErrorAttributes} instance that does not include the
* "exception" attribute.
*/
public DefaultErrorAttributes() {
this(false);
}
/**
* Create a new {@link DefaultErrorAttributes} instance.
* @param includeException whether to include the "exception" attribute
*/
public DefaultErrorAttributes(boolean includeException) {
this.includeException = includeException;
}
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("path", request.path());
Throwable error = getError(request);
if (this.includeException) {
errorAttributes.put("exception", error.getClass().getName());
}
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
addErrorMessage(errorAttributes, error);
if (error instanceof ResponseStatusException) {
HttpStatus errorStatus = ((ResponseStatusException) error).getStatus();
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("error", errorStatus.getReasonPhrase());
}
else {
errorAttributes.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
errorAttributes.put("error", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
}
return errorAttributes;
}
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
errorAttributes.put("trace", stackTrace.toString());
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
errorAttributes.put("message", error.getMessage());
if (error instanceof BindingResult) {
BindingResult result = (BindingResult) error;
if (result.getErrorCount() > 0) {
errorAttributes.put("errors", result.getAllErrors());
}
}
}
@Override
public Throwable getError(ServerRequest request) {
return (Throwable) request.attribute(ERROR_ATTRIBUTE)
.orElseThrow(() -> new IllegalStateException("Missing exception attribute in ServerWebExchange"));
}
@Override
public void storeErrorInformation(Throwable error, ServerWebExchange exchange) {
exchange.getAttributes().putIfAbsent(ERROR_ATTRIBUTE, error);
}
}

@ -0,0 +1,186 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
/**
* Basic global {@link org.springframework.web.server.WebExceptionHandler},
* rendering {@link ErrorAttributes}.
*
* <p>More specific errors can be handled either using Spring WebFlux abstractions
* (e.g. {@code @ExceptionHandler} with the annotation model) or by adding
* {@link RouterFunction} to the chain.
*
* <p>This implementation will render error as HTML views if the client explicitly
* supports that media type. It 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>
* <li>{@code '/<templates>/error/error'}</li>
* <li>{@code '/<static>/error/error.html'}</li>
* </ul>
*
* <p>If none found, a default "Whitelabel Error" HTML view will be rendered.
*
* <p>If the client doesn't support HTML, the error information will be rendered
* as a JSON payload.
*
* @author Brian Clozel
* @since 2.0.0
*/
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
private static final Map<HttpStatus.Series, String> SERIES_VIEWS;
static {
Map<HttpStatus.Series, String> views = new HashMap<>();
views.put(HttpStatus.Series.CLIENT_ERROR, "4xx");
views.put(HttpStatus.Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private final ErrorProperties errorProperties;
/**
* Create a new {@code DefaultErrorWebExceptionHandler} instance.
*
* @param errorAttributes the error attributes
* @param resourceProperties the resources configuration properties
* @param errorProperties the error configuration properties
* @param applicationContext the current application context
*/
public DefaultErrorWebExceptionHandler(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties, ErrorProperties errorProperties,
ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, applicationContext);
this.errorProperties = errorProperties;
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions
.route(acceptsTextHtml(), this::renderErrorView)
.andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* Render the error information as an HTML view.
* @param request the current request
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.TEXT_HTML);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
HttpStatus errorStatus = getHttpStatus(error);
ServerResponse.BodyBuilder response = ServerResponse.status(errorStatus)
.contentType(MediaType.TEXT_HTML);
return Flux.just("error/" + errorStatus.toString(),
"error/" + SERIES_VIEWS.get(errorStatus.series()),
"error/error")
.flatMap(viewName -> renderErrorView(viewName, response, error))
.switchIfEmpty(renderDefaultErrorView(response, error))
.next();
}
/**
* Render the error information as a JSON payload.
* @param request the current request
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
return ServerResponse.status(getHttpStatus(error))
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(error));
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(ServerRequest request,
MediaType produces) {
ErrorProperties.IncludeStacktrace include = this.errorProperties.getIncludeStacktrace();
if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {
return true;
}
if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {
return isTraceEnabled(request);
}
return false;
}
/**
* Get the HTTP error status information from the error map.
* @param errorAttributes the current error information
* @return the error HTTP status
*/
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
int statusCode = (int) errorAttributes.get("status");
return HttpStatus.valueOf(statusCode);
}
/**
* Predicate that checks whether the current request
* explicitly support {@code "text/html"} media type.
* <p>The "match-all" media type is not considered here.
* @return the request predicate
*/
protected RequestPredicate acceptsTextHtml() {
return serverRequest -> {
List<MediaType> acceptedMediaTypes = serverRequest.headers().accept();
acceptedMediaTypes.remove(MediaType.ALL);
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
return acceptedMediaTypes.stream().anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
};
}
}

@ -0,0 +1,58 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.util.Map;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
/**
* Provides access to error attributes which can be logged or presented to the user.
*
* @author Brian Clozel
* @since 2.0
* @see DefaultErrorAttributes
*/
public interface ErrorAttributes {
/**
* Return a {@link Map} of the error attributes. The map can be used as the model of
* an error page, or returned as a {@link ServerResponse} body.
* @param request the source request
* @param includeStackTrace if stack trace elements should be included
* @return a map of error attributes
*/
Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace);
/**
* Return the underlying cause of the error or {@code null} if the error
* cannot be extracted.
* @param request the source ServerRequest
* @return the {@link Exception} that caused the error or {@code null}
*/
Throwable getError(ServerRequest request);
/**
* Store the given error information in the current {@link ServerWebExchange}.
* @param error the {@link Exception} that caused the error
* @param exchange the source exchange
*/
void storeErrorInformation(Throwable error, ServerWebExchange exchange);
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-2017 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.reactive.error;
import org.springframework.web.server.WebExceptionHandler;
/**
* Marker interface that indicates that a {@link WebExceptionHandler}
* is used to render errors.
*
* @author Brian Clozel
* @since 2.0.0
*/
public interface ErrorWebExceptionHandler extends WebExceptionHandler {
}

@ -0,0 +1,96 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} to render errors via a
* WebFlux {@link org.springframework.web.server.WebExceptionHandler}.
*
* @author Brian Clozel
* @since 2.0.0
*/
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass(WebFluxConfigurer.class)
@AutoConfigureBefore(WebFluxAutoConfiguration.class)
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorWebFluxAutoConfiguration {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorWebFluxAutoConfiguration(ServerProperties serverProperties,
ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.getIfAvailable(() -> Collections.emptyList());
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
DefaultErrorWebExceptionHandler exceptionHandler =
new DefaultErrorWebExceptionHandler(errorAttributes, this.resourceProperties,
this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
}

@ -0,0 +1,20 @@
/*
* Copyright 2012-2017 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.
*/
/**
* Auto-configuration for for Spring WebFlux error handling.
*/
package org.springframework.boot.autoconfigure.web.reactive.error;

@ -51,6 +51,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
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.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
@ -302,7 +303,7 @@ public class ErrorMvcAutoConfiguration {
}
/**
* {@link ServletWebServerFactoryCustomizer} that configures the server's error pages.
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

@ -116,6 +116,7 @@ org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\

@ -0,0 +1,180 @@
/*
* Copyright 2012-2017 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.reactive.error;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultErrorAttributes}.
*
* @author Brian Clozel
*/
public class DefaultErrorAttributesTests {
private static final ResponseStatusException NOT_FOUND = new ResponseStatusException(HttpStatus.NOT_FOUND);
@Rule
public ExpectedException thrown = ExpectedException.none();
private DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
private List<HttpMessageReader<?>> readers = ServerCodecConfigurer.create().getReaders();
@Test
public void missingExceptionAttribute() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("Missing exception attribute in ServerWebExchange");
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test").build());
ServerRequest request = ServerRequest.create(exchange, this.readers);
this.errorAttributes.getErrorAttributes(request, false);
}
@Test
public void includeTimeStamp() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, NOT_FOUND), false);
assertThat(attributes.get("timestamp")).isInstanceOf(Date.class);
}
@Test
public void defaultStatusCode() throws Exception {
Error error = new OutOfMemoryError("Test error");
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, error), false);
assertThat(attributes.get("error"))
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
assertThat(attributes.get("status")).isEqualTo(500);
}
@Test
public void includeStatusCode() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, NOT_FOUND), false);
assertThat(attributes.get("error"))
.isEqualTo(HttpStatus.NOT_FOUND.getReasonPhrase());
assertThat(attributes.get("status")).isEqualTo(404);
}
@Test
public void getError() throws Exception {
Error error = new OutOfMemoryError("Test error");
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
ServerRequest serverRequest = buildServerRequest(request, error);
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(serverRequest, false);
assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error);
assertThat(attributes.get("exception")).isNull();
assertThat(attributes.get("message")).isEqualTo("Test error");
}
@Test
public void includeException() throws Exception {
RuntimeException error = new RuntimeException("Test");
this.errorAttributes = new DefaultErrorAttributes(true);
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
ServerRequest serverRequest = buildServerRequest(request, error);
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(serverRequest, false);
assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error);
assertThat(attributes.get("exception")).isEqualTo(RuntimeException.class.getName());
assertThat(attributes.get("message")).isEqualTo("Test");
}
@Test
public void notIncludeTrace() throws Exception {
RuntimeException ex = new RuntimeException("Test");
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, ex), false);
assertThat(attributes.get("trace")).isNull();
}
@Test
public void includeTrace() throws Exception {
RuntimeException ex = new RuntimeException("Test");
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, ex), true);
assertThat(attributes.get("trace").toString()).startsWith("java.lang");
}
@Test
public void includePath() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, NOT_FOUND), false);
assertThat(attributes.get("path")).isEqualTo("/test");
}
@Test
public void extractBindingResultErrors() throws Exception {
Method method = getClass().getMethod("method", String.class);
MethodParameter stringParam = new MethodParameter(method, 0);
BindingResult bindingResult = new MapBindingResult(
Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new WebExchangeBindException(stringParam, bindingResult);
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes
.getErrorAttributes(buildServerRequest(request, ex), false);
assertThat(attributes.get("message"))
.asString().startsWith("Validation failed for argument at index 0 in method: " +
"public int org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorAttributesTests" +
".method(java.lang.String), with 1 error(s)");
assertThat(attributes.get("errors")).isEqualTo(bindingResult.getAllErrors());
}
private ServerRequest buildServerRequest(MockServerHttpRequest request, Throwable error) {
ServerWebExchange exchange = MockServerWebExchange.from(request);
this.errorAttributes.storeErrorInformation(error, exchange);
return ServerRequest.create(exchange, this.readers);
}
public int method(String firstParam) {
return 42;
}
}

@ -0,0 +1,236 @@
/*
* Copyright 2012-2017 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.reactive.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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.validation.Valid;
import org.junit.After;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
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.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link DefaultErrorWebExceptionHandler}
*
* @author Brian Clozel
*/
public class DefaultErrorWebExceptionHandlerIntegrationTest {
private ConfigurableApplicationContext context;
private WebTestClient webTestClient;
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void jsonError() throws Exception {
load();
this.webTestClient.get().uri("/").exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().jsonPath("status").isEqualTo("500")
.jsonPath("error").isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.jsonPath("path").isEqualTo(("/"))
.jsonPath("message").isEqualTo("Expected!")
.jsonPath("exception").doesNotExist()
.jsonPath("trace").doesNotExist();
}
@Test
public void notFound() throws Exception {
load();
this.webTestClient.get().uri("/notFound").exchange()
.expectStatus().isEqualTo(HttpStatus.NOT_FOUND)
.expectBody().jsonPath("status").isEqualTo("404")
.jsonPath("error").isEqualTo(HttpStatus.NOT_FOUND.getReasonPhrase())
.jsonPath("path").isEqualTo(("/notFound"))
.jsonPath("exception").doesNotExist();
}
@Test
public void htmlError() throws Exception {
load();
String body = this.webTestClient.get().uri("/").accept(MediaType.TEXT_HTML).exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectHeader().contentType(MediaType.TEXT_HTML)
.expectBody(String.class).returnResult().getResponseBody();
assertThat(body).contains("status: 500").contains("message: Expected!");
}
@Test
public void bindingResultError() throws Exception {
load();
this.webTestClient.post().uri("/bind")
.contentType(MediaType.APPLICATION_JSON)
.syncBody("{}")
.exchange()
.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST)
.expectBody().jsonPath("status").isEqualTo("400")
.jsonPath("error").isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase())
.jsonPath("path").isEqualTo(("/bind"))
.jsonPath("exception").doesNotExist()
.jsonPath("errors").isArray()
.jsonPath("message").isNotEmpty();
}
@Test
public void includeStackTraceOnParam() throws Exception {
load("--server.error.include-exception=true",
"--server.error.include-stacktrace=on-trace-param");
this.webTestClient.get().uri("/?trace=true").exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().jsonPath("status").isEqualTo("500")
.jsonPath("error").isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.jsonPath("exception").isEqualTo(IllegalStateException.class.getName())
.jsonPath("trace").exists();
}
@Test
public void alwaysIncludeStackTrace() throws Exception {
load("--server.error.include-exception=true",
"--server.error.include-stacktrace=always");
this.webTestClient.get().uri("/?trace=false").exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().jsonPath("status").isEqualTo("500")
.jsonPath("error").isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.jsonPath("exception").isEqualTo(IllegalStateException.class.getName())
.jsonPath("trace").exists();
}
@Test
public void neverIncludeStackTrace() throws Exception {
load("--server.error.include-exception=true",
"--server.error.include-stacktrace=never");
this.webTestClient.get().uri("/?trace=true").exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectBody().jsonPath("status").isEqualTo("500")
.jsonPath("error").isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.jsonPath("exception").isEqualTo(IllegalStateException.class.getName())
.jsonPath("trace").doesNotExist();
}
@Test
public void statusException() throws Exception {
load("--server.error.include-exception=true");
this.webTestClient.get().uri("/badRequest").exchange()
.expectStatus().isEqualTo(HttpStatus.BAD_REQUEST)
.expectBody().jsonPath("status").isEqualTo("400")
.jsonPath("error").isEqualTo(HttpStatus.BAD_REQUEST.getReasonPhrase())
.jsonPath("exception").isEqualTo(ResponseStatusException.class.getName());
}
@Test
public void defaultErrorView() throws Exception {
load("--spring.mustache.prefix=classpath:/unknown/");
String body = this.webTestClient.get().uri("/").accept(MediaType.TEXT_HTML).exchange()
.expectStatus().isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR)
.expectHeader().contentType(MediaType.TEXT_HTML)
.expectBody(String.class).returnResult().getResponseBody();
assertThat(body).contains("Whitelabel Error Page").contains("<div>Expected!</div>");
}
private void load(String... arguments) {
List<String> args = new ArrayList<>();
args.add("--server.port=0");
if (arguments != null) {
args.addAll(Arrays.asList(arguments));
}
SpringApplication application = new SpringApplication(Application.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
this.context = application.run(args.toArray(new String[args.size()]));
this.webTestClient = WebTestClient.bindToApplicationContext(this.context).build();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ReactiveWebServerAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
WebFluxAutoConfiguration.class, ErrorWebFluxAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class})
private @interface MinimalWebConfiguration {
}
@Configuration
@MinimalWebConfiguration
@Import(MustacheAutoConfiguration.class)
public static class Application {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application.class);
application.setWebApplicationType(WebApplicationType.REACTIVE);
application.run(args);
}
@RestController
protected static class ErrorController {
@GetMapping("/")
public String home() {
throw new IllegalStateException("Expected!");
}
@GetMapping("/badRequest")
public Mono<String> badRequest() {
return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST));
}
@PostMapping(path = "/bind", produces = "application/json")
@ResponseBody
public String bodyValidation(@Valid @RequestBody DummyBody body) {
return body.getContent();
}
}
}
}

@ -0,0 +1,34 @@
/*
* Copyright 2012-2017 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.reactive.error;
import javax.validation.constraints.NotNull;
public class DummyBody {
@NotNull
private String content;
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}

@ -0,0 +1,8 @@
<html>
<body>
<ul>
<li>status: {{status}}</li>
<li>message: {{message}}</li>
</ul>
</body>
</html>

@ -2242,7 +2242,7 @@ method:
[[boot-features-spring-webflux]]
[[boot-features-webflux]]
=== The '`Spring WebFlux framework`'
Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0.
@ -2325,7 +2325,7 @@ behavior has been chosen because many Spring developers will add
[[boot-features-spring-webflux-auto-configuration]]
[[boot-features-webflux-auto-configuration]]
==== Spring WebFlux auto-configuration
Spring Boot provides auto-configuration for Spring WebFlux that works well with most
applications.
@ -2345,7 +2345,7 @@ If you want to take complete control of Spring WebFlux, you can add your own
[[boot-features-spring-webflux-httpcodecs]]
[[boot-features-webflux-httpcodecs]]
==== HTTP codecs with HttpMessageReaders and HttpMessageWriters
Spring WebFlux uses the `HttpMessageReader` and `HttpMessageWriter` interface to convert
HTTP requests and responses. They are configured with `CodecConfigurer` with sensible
@ -2378,7 +2378,7 @@ You can also leverage <<boot-features-json-components,Boot's custom JSON seriali
[[boot-features-spring-webflux-static-content]]
[[boot-features-webflux-static-content]]
==== Static Content
By default Spring Boot will serve static content from a directory called `/static` (or
`/public` or `/resources` or `/META-INF/resources`) in the classpath.
@ -2410,7 +2410,7 @@ be deployed as war and have no use of the `src/main/webapp` directory.
[[boot-features-spring-webflux-template-engines]]
[[boot-features-webflux-template-engines]]
==== Template engines
As well as REST web services, you can also use Spring WebFlux to serve dynamic HTML
content. Spring WebFlux supports a variety of templating technologies including Thymeleaf,
@ -2427,6 +2427,89 @@ templates will be picked up automatically from `src/main/resources/templates`.
[[boot-features-webflux-error-handling]]
==== Error Handling
Spring Boot provides a `WebExceptionHandler` that handles all errors in a sensible way;
it is ordered right before the ones provided by WebFlux, which are considered as last
resort. For machine clients it will produce a JSON response with details of the error,
the HTTP status and the exception message. For browser clients there is a '`whitelabel`'
error handler that renders the same data in HTML format. You can also provide your own
HTML templates to display errors
(see <<boot-features-webflux-error-handling-custom-error-pages,next section>>).
The first step to customizing this feature is often about using the existing mechanism
but replacing or augmenting the error contents. For that, you can simply add a bean
of type `ErrorAttributes`.
To change the error handling behavior, you can implement `ErrorWebExceptionHandler` and
register a bean definition of that type; because a `WebExceptionHandler` is quite
low-level, Spring Boot also provides a convenient `AbstractErrorWebExceptionHandler`
to let you handle errors in a WebFlux functional way:
[source,java,indent=0,subs="verbatim,quotes,attributes"]
----
public class CustomErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
// Define constructor here
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions
.route(aPredicate, aHandler)
.andRoute(anotherPredicate, anotherHandler);
}
}
----
For a more complete picture, you can also subclass `DefaultErrorWebExceptionHandler`
directly and override specific methods.
[[boot-features-webflux-error-handling-custom-error-pages]]
===== Custom error pages
If you want to display a custom HTML error page for a given status code, you can 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>
----
To map all `5xx` errors using a Mustache template, you'd have a structure like this:
[source,indent=0,subs="verbatim,quotes,attributes"]
----
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.mustache
+- <other templates>
----
[[boot-features-jersey]]
=== JAX-RS and Jersey
If you prefer the JAX-RS programming model for REST endpoints you can use one of the
@ -5030,7 +5113,7 @@ out-of-the-box experience. See the
Spring Boot will create and pre-configure such a builder for you; for example,
client HTTP codecs will be configured just like the server ones
(see <<boot-features-spring-webflux-httpcodecs,WebFlux HTTP codecs auto-configuration>>).
(see <<boot-features-webflux-httpcodecs,WebFlux HTTP codecs auto-configuration>>).
Here's a typical example:

Loading…
Cancel
Save