001/* 002 * Copyright 2002-2020 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 * https://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.web.server.adapter; 018 019import java.util.Arrays; 020import java.util.HashSet; 021import java.util.Set; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025import reactor.core.publisher.Mono; 026 027import org.springframework.context.ApplicationContext; 028import org.springframework.core.NestedExceptionUtils; 029import org.springframework.core.log.LogFormatUtils; 030import org.springframework.http.HttpHeaders; 031import org.springframework.http.HttpStatus; 032import org.springframework.http.codec.LoggingCodecSupport; 033import org.springframework.http.codec.ServerCodecConfigurer; 034import org.springframework.http.server.reactive.HttpHandler; 035import org.springframework.http.server.reactive.ServerHttpRequest; 036import org.springframework.http.server.reactive.ServerHttpResponse; 037import org.springframework.lang.Nullable; 038import org.springframework.util.Assert; 039import org.springframework.util.StringUtils; 040import org.springframework.web.server.ServerWebExchange; 041import org.springframework.web.server.WebHandler; 042import org.springframework.web.server.handler.WebHandlerDecorator; 043import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; 044import org.springframework.web.server.i18n.LocaleContextResolver; 045import org.springframework.web.server.session.DefaultWebSessionManager; 046import org.springframework.web.server.session.WebSessionManager; 047 048/** 049 * Default adapter of {@link WebHandler} to the {@link HttpHandler} contract. 050 * 051 * <p>By default creates and configures a {@link DefaultServerWebExchange} and 052 * then invokes the target {@code WebHandler}. 053 * 054 * @author Rossen Stoyanchev 055 * @author Sebastien Deleuze 056 * @since 5.0 057 */ 058public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler { 059 060 /** 061 * Dedicated log category for disconnected client exceptions. 062 * <p>Servlet containers don't expose a client disconnected callback; see 063 * <a href="https://github.com/eclipse-ee4j/servlet-api/issues/44">eclipse-ee4j/servlet-api#44</a>. 064 * <p>To avoid filling logs with unnecessary stack traces, we make an 065 * effort to identify such network failures on a per-server basis, and then 066 * log under a separate log category a simple one-line message at DEBUG level 067 * or a full stack trace only at TRACE level. 068 */ 069 private static final String DISCONNECTED_CLIENT_LOG_CATEGORY = 070 "org.springframework.web.server.DisconnectedClient"; 071 072 // Similar declaration exists in AbstractSockJsSession.. 073 private static final Set<String> DISCONNECTED_CLIENT_EXCEPTIONS = new HashSet<>( 074 Arrays.asList("AbortedException", "ClientAbortException", "EOFException", "EofException")); 075 076 077 private static final Log logger = LogFactory.getLog(HttpWebHandlerAdapter.class); 078 079 private static final Log lostClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY); 080 081 082 private WebSessionManager sessionManager = new DefaultWebSessionManager(); 083 084 private ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create(); 085 086 private LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver(); 087 088 @Nullable 089 private ForwardedHeaderTransformer forwardedHeaderTransformer; 090 091 @Nullable 092 private ApplicationContext applicationContext; 093 094 /** Whether to log potentially sensitive info (form data at DEBUG, headers at TRACE). */ 095 private boolean enableLoggingRequestDetails = false; 096 097 098 public HttpWebHandlerAdapter(WebHandler delegate) { 099 super(delegate); 100 } 101 102 103 /** 104 * Configure a custom {@link WebSessionManager} to use for managing web 105 * sessions. The provided instance is set on each created 106 * {@link DefaultServerWebExchange}. 107 * <p>By default this is set to {@link DefaultWebSessionManager}. 108 * @param sessionManager the session manager to use 109 */ 110 public void setSessionManager(WebSessionManager sessionManager) { 111 Assert.notNull(sessionManager, "WebSessionManager must not be null"); 112 this.sessionManager = sessionManager; 113 } 114 115 /** 116 * Return the configured {@link WebSessionManager}. 117 */ 118 public WebSessionManager getSessionManager() { 119 return this.sessionManager; 120 } 121 122 /** 123 * Configure a custom {@link ServerCodecConfigurer}. The provided instance is set on 124 * each created {@link DefaultServerWebExchange}. 125 * <p>By default this is set to {@link ServerCodecConfigurer#create()}. 126 * @param codecConfigurer the codec configurer to use 127 */ 128 public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) { 129 Assert.notNull(codecConfigurer, "ServerCodecConfigurer is required"); 130 this.codecConfigurer = codecConfigurer; 131 132 this.enableLoggingRequestDetails = false; 133 this.codecConfigurer.getReaders().stream() 134 .filter(LoggingCodecSupport.class::isInstance) 135 .forEach(reader -> { 136 if (((LoggingCodecSupport) reader).isEnableLoggingRequestDetails()) { 137 this.enableLoggingRequestDetails = true; 138 } 139 }); 140 } 141 142 /** 143 * Return the configured {@link ServerCodecConfigurer}. 144 */ 145 public ServerCodecConfigurer getCodecConfigurer() { 146 return this.codecConfigurer; 147 } 148 149 /** 150 * Configure a custom {@link LocaleContextResolver}. The provided instance is set on 151 * each created {@link DefaultServerWebExchange}. 152 * <p>By default this is set to 153 * {@link org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver}. 154 * @param resolver the locale context resolver to use 155 */ 156 public void setLocaleContextResolver(LocaleContextResolver resolver) { 157 Assert.notNull(resolver, "LocaleContextResolver is required"); 158 this.localeContextResolver = resolver; 159 } 160 161 /** 162 * Return the configured {@link LocaleContextResolver}. 163 */ 164 public LocaleContextResolver getLocaleContextResolver() { 165 return this.localeContextResolver; 166 } 167 168 /** 169 * Enable processing of forwarded headers, either extracting and removing, 170 * or remove only. 171 * <p>By default this is not set. 172 * @param transformer the transformer to use 173 * @since 5.1 174 */ 175 public void setForwardedHeaderTransformer(ForwardedHeaderTransformer transformer) { 176 Assert.notNull(transformer, "ForwardedHeaderTransformer is required"); 177 this.forwardedHeaderTransformer = transformer; 178 } 179 180 /** 181 * Return the configured {@link ForwardedHeaderTransformer}. 182 * @since 5.1 183 */ 184 @Nullable 185 public ForwardedHeaderTransformer getForwardedHeaderTransformer() { 186 return this.forwardedHeaderTransformer; 187 } 188 189 /** 190 * Configure the {@code ApplicationContext} associated with the web application, 191 * if it was initialized with one via 192 * {@link org.springframework.web.server.adapter.WebHttpHandlerBuilder#applicationContext(ApplicationContext)}. 193 * @param applicationContext the context 194 * @since 5.0.3 195 */ 196 public void setApplicationContext(ApplicationContext applicationContext) { 197 this.applicationContext = applicationContext; 198 } 199 200 /** 201 * Return the configured {@code ApplicationContext}, if any. 202 * @since 5.0.3 203 */ 204 @Nullable 205 public ApplicationContext getApplicationContext() { 206 return this.applicationContext; 207 } 208 209 /** 210 * This method must be invoked after all properties have been set to 211 * complete initialization. 212 */ 213 public void afterPropertiesSet() { 214 if (logger.isDebugEnabled()) { 215 String value = this.enableLoggingRequestDetails ? 216 "shown which may lead to unsafe logging of potentially sensitive data" : 217 "masked to prevent unsafe logging of potentially sensitive data"; 218 logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + 219 "': form data and headers will be " + value); 220 } 221 } 222 223 224 @Override 225 public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { 226 if (this.forwardedHeaderTransformer != null) { 227 request = this.forwardedHeaderTransformer.apply(request); 228 } 229 ServerWebExchange exchange = createExchange(request, response); 230 231 LogFormatUtils.traceDebug(logger, traceOn -> 232 exchange.getLogPrefix() + formatRequest(exchange.getRequest()) + 233 (traceOn ? ", headers=" + formatHeaders(exchange.getRequest().getHeaders()) : "")); 234 235 return getDelegate().handle(exchange) 236 .doOnSuccess(aVoid -> logResponse(exchange)) 237 .onErrorResume(ex -> handleUnresolvedError(exchange, ex)) 238 .then(Mono.defer(response::setComplete)); 239 } 240 241 protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { 242 return new DefaultServerWebExchange(request, response, this.sessionManager, 243 getCodecConfigurer(), getLocaleContextResolver(), this.applicationContext); 244 } 245 246 /** 247 * Format the request for logging purposes including HTTP method and URL. 248 * <p>By default this prints the HTTP method, the URL path, and the query. 249 * @param request the request to format 250 * @return the String to display, never empty or {@code null} 251 */ 252 protected String formatRequest(ServerHttpRequest request) { 253 String rawQuery = request.getURI().getRawQuery(); 254 String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : ""; 255 return "HTTP " + request.getMethod() + " \"" + request.getPath() + query + "\""; 256 } 257 258 private void logResponse(ServerWebExchange exchange) { 259 LogFormatUtils.traceDebug(logger, traceOn -> { 260 HttpStatus status = exchange.getResponse().getStatusCode(); 261 return exchange.getLogPrefix() + "Completed " + (status != null ? status : "200 OK") + 262 (traceOn ? ", headers=" + formatHeaders(exchange.getResponse().getHeaders()) : ""); 263 }); 264 } 265 266 private String formatHeaders(HttpHeaders responseHeaders) { 267 return this.enableLoggingRequestDetails ? 268 responseHeaders.toString() : responseHeaders.isEmpty() ? "{}" : "{masked}"; 269 } 270 271 private Mono<Void> handleUnresolvedError(ServerWebExchange exchange, Throwable ex) { 272 ServerHttpRequest request = exchange.getRequest(); 273 ServerHttpResponse response = exchange.getResponse(); 274 String logPrefix = exchange.getLogPrefix(); 275 276 // Sometimes a remote call error can look like a disconnected client. 277 // Try to set the response first before the "isDisconnectedClient" check. 278 279 if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) { 280 logger.error(logPrefix + "500 Server Error for " + formatRequest(request), ex); 281 return Mono.empty(); 282 } 283 else if (isDisconnectedClientError(ex)) { 284 if (lostClientLogger.isTraceEnabled()) { 285 lostClientLogger.trace(logPrefix + "Client went away", ex); 286 } 287 else if (lostClientLogger.isDebugEnabled()) { 288 lostClientLogger.debug(logPrefix + "Client went away: " + ex + 289 " (stacktrace at TRACE level for '" + DISCONNECTED_CLIENT_LOG_CATEGORY + "')"); 290 } 291 return Mono.empty(); 292 } 293 else { 294 // After the response is committed, propagate errors to the server... 295 logger.error(logPrefix + "Error [" + ex + "] for " + formatRequest(request) + 296 ", but ServerHttpResponse already committed (" + response.getStatusCode() + ")"); 297 return Mono.error(ex); 298 } 299 } 300 301 private boolean isDisconnectedClientError(Throwable ex) { 302 String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); 303 if (message != null) { 304 String text = message.toLowerCase(); 305 if (text.contains("broken pipe") || text.contains("connection reset by peer")) { 306 return true; 307 } 308 } 309 return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()); 310 } 311 312}