diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java index 97aa048d74b..c944a1fdebf 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -19,14 +19,18 @@ package org.springframework.web.socket.sockjs.transport.session; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ScheduledFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.NestedCheckedException; import org.springframework.util.Assert; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -48,6 +52,40 @@ public abstract class AbstractSockJsSession implements SockJsSession { protected final Log logger = LogFactory.getLog(getClass()); + /** + * Log category to use on network IO exceptions after a client has gone away. + * + *

The Servlet API does not provide notifications when a client disconnects, + * see see https://java.net/jira/browse/SERVLET_SPEC-44. Therefore network IO + * failures may occur simply because a client has gone away and that can fill + * the logs with unnecessary stack traces. + * + *

We make a best effort to identify such network failures, on a per-server + * basis, and log them under a separate log category. A simple one-line message + * is logged at DEBUG level while a full stack trace is shown at TRACE level. + * + * @see #disconnectedClientLogger + */ + public static final String DISCONNECTED_CLIENT_LOG_CATEGORY = + "org.springframework.web.socket.sockjs.DisconnectedClient"; + + /** + * Separate logger to use on network IO failure after a client has gone away. + * @see #DISCONNECTED_CLIENT_LOG_CATEGORY + */ + protected static final Log disconnectedClientLogger = LogFactory.getLog(DISCONNECTED_CLIENT_LOG_CATEGORY); + + + private final static Set disconnectedClientExceptions = + Collections.newSetFromMap(new HashMap(2)); + + static { + disconnectedClientExceptions.add("ClientAbortException"); // Tomcat + disconnectedClientExceptions.add("EofException"); // Jetty + // IOException("Broken pipe") on WildFly and Glassfish + } + + private final String id; private final SockJsServiceConfig config; @@ -273,9 +311,7 @@ public abstract class AbstractSockJsSession implements SockJsSession { writeFrameInternal(frame); } catch (Throwable ex) { - logger.error("Terminating connection after failure to send message to client. " + - "This may be because the client has gone away " + - "(see https://java.net/jira/browse/SERVLET_SPEC-44)", ex); + logWriteFrameFailure(ex); try { disconnect(CloseStatus.SERVER_ERROR); close(CloseStatus.SERVER_ERROR); @@ -287,6 +323,28 @@ public abstract class AbstractSockJsSession implements SockJsSession { } } + private void logWriteFrameFailure(Throwable failure) { + + @SuppressWarnings("serial") + NestedCheckedException nestedException = new NestedCheckedException("", failure) {}; + + if ("Broken pipe".equalsIgnoreCase(nestedException.getMostSpecificCause().getMessage()) || + disconnectedClientExceptions.contains(failure.getClass().getSimpleName())) { + + if (disconnectedClientLogger.isTraceEnabled()) { + disconnectedClientLogger.trace("Looks like the client has gone away", failure); + } + else if (disconnectedClientLogger.isDebugEnabled()) { + disconnectedClientLogger.debug("Looks like the client has gone away: " + + nestedException.getMessage() + " (For full stack trace, raise '" + + DISCONNECTED_CLIENT_LOG_CATEGORY + "' log category at TRACE level)"); + } + } + else { + logger.error("Terminating connection after failure to send message to client.", failure); + } + } + protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException; public synchronized void sendHeartbeat() throws SockJsTransportFailureException { @@ -314,7 +372,7 @@ public abstract class AbstractSockJsSession implements SockJsSession { } }, time); if (logger.isTraceEnabled()) { - logger.trace("Scheduled heartbeat after " + this.config.getHeartbeatTime()/1000 + " seconds"); + logger.trace("Scheduled heartbeat after " + this.config.getHeartbeatTime() / 1000 + " seconds"); } } diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 7e85e0ab30f..efebb125984 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -37199,13 +37199,19 @@ relies on Servlet 3 async support. [WARNING] ==== -Servlet 3 async support does not expose a notification when a client disconnects, -e.g. a browser tab is closed, page is refreshed, etc -(see https://java.net/jira/browse/SERVLET_SPEC-44[SERVLET_SPEC-44]). However, the -Serlvet container will usually raise an IOException on the next attempt to write -to the response; at which point the SockJS session will be closed. Since the +The Servlet API does not provide notifications when a client disconnects, +see https://java.net/jira/browse/SERVLET_SPEC-44[SERVLET_SPEC-44]. However, +Serlvet containers typically raise an IOException on the next attempt to write +to the response at which point the SockJS session is closed. Since the SockJsService sends a heartbeat every 25 seconds, typically a disconnected -client will be detected within that time period. +client should be detected within that time period. + +This also means that network IO failures may occur simply because a client has gone +away and that can fill the logs with unnecessary stack traces. +We make a best effort to identify such network failures, on a per-server basis, and log +them under a separate log category, see `AbstractSockJsSession#DISCONNECTED_CLIENT_LOG_CATEGORY`. +A simple one-line message is logged at DEBUG level using this category while a full +stack trace is shown at TRACE level. ====