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}