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-8625pull/10616/merge
parent
c4adb76df2
commit
68db43cf44
@ -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;
|
@ -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>
|
Loading…
Reference in New Issue