001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.autoconfigure.web.reactive.error;
018
019import java.util.Collections;
020import java.util.Date;
021import java.util.List;
022import java.util.Map;
023
024import reactor.core.publisher.Mono;
025
026import org.springframework.beans.factory.InitializingBean;
027import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
028import org.springframework.boot.autoconfigure.web.ResourceProperties;
029import org.springframework.boot.web.reactive.error.ErrorAttributes;
030import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
031import org.springframework.context.ApplicationContext;
032import org.springframework.core.io.Resource;
033import org.springframework.http.codec.HttpMessageReader;
034import org.springframework.http.codec.HttpMessageWriter;
035import org.springframework.util.Assert;
036import org.springframework.util.CollectionUtils;
037import org.springframework.web.reactive.function.BodyInserters;
038import org.springframework.web.reactive.function.server.RouterFunction;
039import org.springframework.web.reactive.function.server.ServerRequest;
040import org.springframework.web.reactive.function.server.ServerResponse;
041import org.springframework.web.reactive.result.view.ViewResolver;
042import org.springframework.web.server.ServerWebExchange;
043import org.springframework.web.util.HtmlUtils;
044
045/**
046 * Abstract base class for {@link ErrorWebExceptionHandler} implementations.
047 *
048 * @author Brian Clozel
049 * @since 2.0.0
050 * @see ErrorAttributes
051 */
052public abstract class AbstractErrorWebExceptionHandler
053                implements ErrorWebExceptionHandler, InitializingBean {
054
055        private final ApplicationContext applicationContext;
056
057        private final ErrorAttributes errorAttributes;
058
059        private final ResourceProperties resourceProperties;
060
061        private final TemplateAvailabilityProviders templateAvailabilityProviders;
062
063        private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
064
065        private List<HttpMessageWriter<?>> messageWriters = Collections.emptyList();
066
067        private List<ViewResolver> viewResolvers = Collections.emptyList();
068
069        public AbstractErrorWebExceptionHandler(ErrorAttributes errorAttributes,
070                        ResourceProperties resourceProperties,
071                        ApplicationContext applicationContext) {
072                Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
073                Assert.notNull(resourceProperties, "ResourceProperties must not be null");
074                Assert.notNull(applicationContext, "ApplicationContext must not be null");
075                this.errorAttributes = errorAttributes;
076                this.resourceProperties = resourceProperties;
077                this.applicationContext = applicationContext;
078                this.templateAvailabilityProviders = new TemplateAvailabilityProviders(
079                                applicationContext);
080        }
081
082        /**
083         * Configure HTTP message writers to serialize the response body with.
084         * @param messageWriters the {@link HttpMessageWriter}s to use
085         */
086        public void setMessageWriters(List<HttpMessageWriter<?>> messageWriters) {
087                Assert.notNull(messageWriters, "'messageWriters' must not be null");
088                this.messageWriters = messageWriters;
089        }
090
091        /**
092         * Configure HTTP message readers to deserialize the request body with.
093         * @param messageReaders the {@link HttpMessageReader}s to use
094         */
095        public void setMessageReaders(List<HttpMessageReader<?>> messageReaders) {
096                Assert.notNull(messageReaders, "'messageReaders' must not be null");
097                this.messageReaders = messageReaders;
098        }
099
100        /**
101         * Configure the {@link ViewResolver} to use for rendering views.
102         * @param viewResolvers the list of {@link ViewResolver}s to use
103         */
104        public void setViewResolvers(List<ViewResolver> viewResolvers) {
105                this.viewResolvers = viewResolvers;
106        }
107
108        /**
109         * Extract the error attributes from the current request, to be used to populate error
110         * views or JSON payloads.
111         * @param request the source request
112         * @param includeStackTrace whether to include the error stacktrace information
113         * @return the error attributes as a Map.
114         */
115        protected Map<String, Object> getErrorAttributes(ServerRequest request,
116                        boolean includeStackTrace) {
117                return this.errorAttributes.getErrorAttributes(request, includeStackTrace);
118        }
119
120        /**
121         * Extract the original error from the current request.
122         * @param request the source request
123         * @return the error
124         */
125        protected Throwable getError(ServerRequest request) {
126                return this.errorAttributes.getError(request);
127        }
128
129        /**
130         * Check whether the trace attribute has been set on the given request.
131         * @param request the source request
132         * @return {@code true} if the error trace has been requested, {@code false} otherwise
133         */
134        protected boolean isTraceEnabled(ServerRequest request) {
135                String parameter = request.queryParam("trace").orElse("false");
136                return !"false".equalsIgnoreCase(parameter);
137        }
138
139        /**
140         * Render the given error data as a view, using a template view if available or a
141         * static HTML file if available otherwise. This will return an empty
142         * {@code Publisher} if none of the above are available.
143         * @param viewName the view name
144         * @param responseBody the error response being built
145         * @param error the error data as a map
146         * @return a Publisher of the {@link ServerResponse}
147         */
148        protected Mono<ServerResponse> renderErrorView(String viewName,
149                        ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {
150                if (isTemplateAvailable(viewName)) {
151                        return responseBody.render(viewName, error);
152                }
153                Resource resource = resolveResource(viewName);
154                if (resource != null) {
155                        return responseBody.body(BodyInserters.fromResource(resource));
156                }
157                return Mono.empty();
158        }
159
160        private boolean isTemplateAvailable(String viewName) {
161                return this.templateAvailabilityProviders.getProvider(viewName,
162                                this.applicationContext) != null;
163        }
164
165        private Resource resolveResource(String viewName) {
166                for (String location : this.resourceProperties.getStaticLocations()) {
167                        try {
168                                Resource resource = this.applicationContext.getResource(location);
169                                resource = resource.createRelative(viewName + ".html");
170                                if (resource.exists()) {
171                                        return resource;
172                                }
173                        }
174                        catch (Exception ex) {
175                                // Ignore
176                        }
177                }
178                return null;
179        }
180
181        /**
182         * Render a default HTML "Whitelabel Error Page".
183         * <p>
184         * Useful when no other error view is available in the application.
185         * @param responseBody the error response being built
186         * @param error the error data as a map
187         * @return a Publisher of the {@link ServerResponse}
188         */
189        protected Mono<ServerResponse> renderDefaultErrorView(
190                        ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {
191                StringBuilder builder = new StringBuilder();
192                Date timestamp = (Date) error.get("timestamp");
193                Object message = error.get("message");
194                Object trace = error.get("trace");
195                builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
196                                "<p>This application has no configured error view, so you are seeing this as a fallback.</p>")
197                                .append("<div id='created'>").append(timestamp).append("</div>")
198                                .append("<div>There was an unexpected error (type=")
199                                .append(htmlEscape(error.get("error"))).append(", status=")
200                                .append(htmlEscape(error.get("status"))).append(").</div>");
201                if (message != null) {
202                        builder.append("<div>").append(htmlEscape(message)).append("</div>");
203                }
204                if (trace != null) {
205                        builder.append("<div style='white-space:pre-wrap;'>")
206                                        .append(htmlEscape(trace)).append("</div>");
207                }
208                builder.append("</body></html>");
209                return responseBody.syncBody(builder.toString());
210        }
211
212        private String htmlEscape(Object input) {
213                return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
214        }
215
216        @Override
217        public void afterPropertiesSet() throws Exception {
218                if (CollectionUtils.isEmpty(this.messageWriters)) {
219                        throw new IllegalArgumentException("Property 'messageWriters' is required");
220                }
221        }
222
223        /**
224         * Create a {@link RouterFunction} that can route and handle errors as JSON responses
225         * or HTML views.
226         * <p>
227         * If the returned {@link RouterFunction} doesn't route to a {@code HandlerFunction},
228         * the original exception is propagated in the pipeline and can be processed by other
229         * {@link org.springframework.web.server.WebExceptionHandler}s.
230         * @param errorAttributes the {@code ErrorAttributes} instance to use to extract error
231         * information
232         * @return a {@link RouterFunction} that routes and handles errors
233         */
234        protected abstract RouterFunction<ServerResponse> getRoutingFunction(
235                        ErrorAttributes errorAttributes);
236
237        @Override
238        public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
239                if (exchange.getResponse().isCommitted()) {
240                        return Mono.error(throwable);
241                }
242                this.errorAttributes.storeErrorInformation(throwable, exchange);
243                ServerRequest request = ServerRequest.create(exchange, this.messageReaders);
244                return getRoutingFunction(this.errorAttributes).route(request)
245                                .switchIfEmpty(Mono.error(throwable))
246                                .flatMap((handler) -> handler.handle(request))
247                                .flatMap((response) -> write(exchange, response));
248        }
249
250        private Mono<? extends Void> write(ServerWebExchange exchange,
251                        ServerResponse response) {
252                // force content-type since writeTo won't overwrite response header values
253                exchange.getResponse().getHeaders()
254                                .setContentType(response.headers().getContentType());
255                return response.writeTo(exchange, new ResponseContext());
256        }
257
258        private class ResponseContext implements ServerResponse.Context {
259
260                @Override
261                public List<HttpMessageWriter<?>> messageWriters() {
262                        return AbstractErrorWebExceptionHandler.this.messageWriters;
263                }
264
265                @Override
266                public List<ViewResolver> viewResolvers() {
267                        return AbstractErrorWebExceptionHandler.this.viewResolvers;
268                }
269
270        }
271
272}