org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler Maven / Gradle / Ivy
/*
* Copyright 2012-2020 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.autoconfigure.web.reactive.error;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
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.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
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.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.all;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* Basic global {@link org.springframework.web.server.WebExceptionHandler}, rendering
* {@link ErrorAttributes}.
*
* 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.
*
* 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}.
*
* For example, an {@code HTTP 404} will search (in the specific order):
*
* - {@code '/
/error/404.'}
* - {@code '/
/error/404.html'}
* - {@code '/
/error/4xx.'}
* - {@code '/
/error/4xx.html'}
* - {@code '/
/error/error'}
* - {@code '/
/error/error.html'}
*
*
* If none found, a default "Whitelabel Error" HTML view will be rendered.
*
* If the client doesn't support HTML, the error information will be rendered as a JSON
* payload.
*
* @author Brian Clozel
* @author Scott Frederick
* @since 2.0.0
*/
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Map SERIES_VIEWS;
static {
Map views = new EnumMap<>(HttpStatus.Series.class);
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 getRoutingFunction(ErrorAttributes errorAttributes) {
return route(acceptsTextHtml(), this::renderErrorView).andRoute(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 renderErrorView(ServerRequest request) {
Map error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
int errorStatus = getHttpStatus(error);
ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8);
return Flux.just(getData(errorStatus).toArray(new String[] {}))
.flatMap((viewName) -> renderErrorView(viewName, responseBody, error))
.switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled()
? renderDefaultErrorView(responseBody, error) : Mono.error(getError(request)))
.next();
}
private List getData(int errorStatus) {
List data = new ArrayList<>();
data.add("error/" + errorStatus);
HttpStatus.Series series = HttpStatus.Series.resolve(errorStatus);
if (series != null) {
data.add("error/" + SERIES_VIEWS.get(series));
}
data.add("error/error");
return data;
}
/**
* Render the error information as a JSON payload.
* @param request the current request
* @return a {@code Publisher} of the HTTP response
*/
protected Mono renderErrorResponse(ServerRequest request) {
Map error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
}
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
return options;
}
/**
* 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
*/
@SuppressWarnings("deprecation")
protected boolean isIncludeStackTrace(ServerRequest request, MediaType produces) {
switch (this.errorProperties.getIncludeStacktrace()) {
case ALWAYS:
return true;
case ON_PARAM:
case ON_TRACE_PARAM:
return isTraceEnabled(request);
default:
return false;
}
}
/**
* Determine if the message attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the message attribute should be included
*/
protected boolean isIncludeMessage(ServerRequest request, MediaType produces) {
switch (this.errorProperties.getIncludeMessage()) {
case ALWAYS:
return true;
case ON_PARAM:
return isMessageEnabled(request);
default:
return false;
}
}
/**
* Determine if the errors attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the errors attribute should be included
*/
protected boolean isIncludeBindingErrors(ServerRequest request, MediaType produces) {
switch (this.errorProperties.getIncludeBindingErrors()) {
case ALWAYS:
return true;
case ON_PARAM:
return isBindingErrorsEnabled(request);
default:
return false;
}
}
/**
* Get the HTTP error status information from the error map.
* @param errorAttributes the current error information
* @return the error HTTP status
*/
protected int getHttpStatus(Map errorAttributes) {
return (int) errorAttributes.get("status");
}
/**
* Predicate that checks whether the current request explicitly support
* {@code "text/html"} media type.
*
* The "match-all" media type is not considered here.
* @return the request predicate
*/
protected RequestPredicate acceptsTextHtml() {
return (serverRequest) -> {
try {
List acceptedMediaTypes = serverRequest.headers().accept();
acceptedMediaTypes.remove(MediaType.ALL);
MediaType.sortBySpecificityAndQuality(acceptedMediaTypes);
return acceptedMediaTypes.stream().anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
}
catch (InvalidMediaTypeException ex) {
return false;
}
};
}
}