From 15a2f034597e6b19972adf4d29591f64a21b7b1f Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 3 Aug 2013 11:01:07 -0400 Subject: [PATCH] Polish SockJS exception handling and javadoc See javadoc in SockJsService for details. Also remove ReadOnlyMultiValueMap, CollectionUtils has a method for that already. --- .../util/ReadOnlyMultiValueMap.java | 150 ------------------ .../http/server/ServletServerHttpRequest.java | 4 +- ...ingException.java => SockJsException.java} | 13 +- .../SockJsMessageDeliveryException.java | 48 ++++++ .../web/socket/sockjs/SockJsService.java | 37 ++++- .../SockJsTransportFailureException.java | 36 +++++ .../sockjs/support/AbstractSockJsService.java | 85 +++++----- .../sockjs/support/frame/SockJsFrame.java | 3 +- .../support/frame/SockJsMessageCodec.java | 4 +- .../sockjs/transport/TransportHandler.java | 23 ++- .../sockjs/transport/TransportType.java | 2 +- ...AbstractHttpReceivingTransportHandler.java | 45 +++--- .../AbstractHttpSendingTransportHandler.java | 20 +-- .../handler/DefaultSockJsService.java | 5 +- .../handler/HtmlFileTransportHandler.java | 24 +-- .../handler/JsonpPollingTransportHandler.java | 13 +- .../handler/JsonpTransportHandler.java | 13 +- .../handler/SockJsWebSocketHandler.java | 22 +-- .../handler/WebSocketTransportHandler.java | 15 +- .../session/AbstractHttpSockJsSession.java | 50 +++--- .../session/AbstractSockJsSession.java | 59 ++++--- .../session/PollingSockJsSession.java | 7 +- .../session/StreamingSockJsSession.java | 9 +- .../session/WebSocketServerSockJsSession.java | 9 +- .../LoggingWebSocketHandlerDecorator.java | 3 + .../support/AbstractSockJsServiceTests.java | 4 +- .../HttpReceivingTransportHandlerTests.java | 36 ++--- .../AbstractHttpSockJsSessionTests.java | 2 +- .../session/AbstractSockJsSessionTests.java | 68 +++++--- .../session/TestHttpSockJsSession.java | 120 ++++++++++++++ .../transport/session/TestSockJsSession.java | 29 ++-- 31 files changed, 537 insertions(+), 421 deletions(-) delete mode 100644 spring-core/src/main/java/org/springframework/util/ReadOnlyMultiValueMap.java rename spring-websocket/src/main/java/org/springframework/web/socket/sockjs/{SockJsProcessingException.java => SockJsException.java} (72%) create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageDeliveryException.java create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsTransportFailureException.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/TestHttpSockJsSession.java diff --git a/spring-core/src/main/java/org/springframework/util/ReadOnlyMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/ReadOnlyMultiValueMap.java deleted file mode 100644 index b6806eaef2..0000000000 --- a/spring-core/src/main/java/org/springframework/util/ReadOnlyMultiValueMap.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2002-2013 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.util; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - - -/** - * Wraps a {@link MultiValueMap} to make it immutable. - * - * @author Rossen Stoyanchev - * @since 4.0 - */ -public class ReadOnlyMultiValueMap implements MultiValueMap { - - private final MultiValueMap targetMap; - - - /** - * Create a new ReadOnlyMultiValueMap that wraps the given target map. - */ - public ReadOnlyMultiValueMap(MultiValueMap targetMap) { - this.targetMap = targetMap; - } - - // MultiValueMap implementation - - @Override - public void add(K key, V value) { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public V getFirst(K key) { - return this.targetMap.getFirst(key); - } - - @Override - public void set(K key, V value) { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public void setAll(Map values) { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public Map toSingleValueMap() { - return Collections.unmodifiableMap(this.targetMap.toSingleValueMap()); - } - - - // Map implementation - - @Override - public int size() { - return this.targetMap.size(); - } - - @Override - public boolean isEmpty() { - return this.targetMap.isEmpty(); - } - - @Override - public boolean containsKey(Object key) { - return this.targetMap.containsKey(key); - } - - @Override - public boolean containsValue(Object value) { - return this.targetMap.containsValue(value); - } - - @Override - public List get(Object key) { - return this.targetMap.get(key); - } - - @Override - public List put(K key, List value) { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public List remove(Object key) { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public void putAll(Map> m) { - this.targetMap.putAll(m); - } - - @Override - public void clear() { - throw new UnsupportedOperationException("This map is immutable."); - } - - @Override - public Set keySet() { - return Collections.unmodifiableSet(this.targetMap.keySet()); - } - - @Override - public Collection> values() { - return Collections.unmodifiableCollection(this.targetMap.values()); - } - - @Override - public Set>> entrySet() { - return Collections.unmodifiableSet(this.targetMap.entrySet()); - } - - - @Override - public boolean equals(Object obj) { - return this.targetMap.equals(obj); - } - - @Override - public int hashCode() { - return this.targetMap.hashCode(); - } - - @Override - public String toString() { - return this.targetMap.toString(); - } - -} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index c9a30e099d..fa85df5a19 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -44,9 +44,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.ReadOnlyMultiValueMap; /** * {@link ServerHttpRequest} implementation that is based on a {@link HttpServletRequest}. @@ -184,7 +184,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { } } } - this.queryParams = new ReadOnlyMultiValueMap(result); + this.queryParams = CollectionUtils.unmodifiableMultiValueMap(result); } return this.queryParams; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsProcessingException.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsException.java similarity index 72% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsProcessingException.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsException.java index 78d4ac26ee..ccd2f32598 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsProcessingException.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsException.java @@ -19,19 +19,19 @@ package org.springframework.web.socket.sockjs; import org.springframework.core.NestedRuntimeException; /** - * Raised when SockJS request handling fails. + * Base class for exceptions raised while processing SockJS HTTP requests. * * @author Rossen Stoyanchev * @since 4.0 */ @SuppressWarnings("serial") -public class SockJsProcessingException extends NestedRuntimeException { +public class SockJsException extends NestedRuntimeException { private final String sessionId; - public SockJsProcessingException(String msg, Throwable cause, String sessionId) { - super(msg, cause); + public SockJsException(String message, String sessionId, Throwable cause) { + super(message, cause); this.sessionId = sessionId; } @@ -39,9 +39,4 @@ public class SockJsProcessingException extends NestedRuntimeException { return this.sessionId; } - @Override - public String getMessage() { - return "Transport error for SockJS session id=" + this.sessionId + ", " + super.getMessage(); - } - } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageDeliveryException.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageDeliveryException.java new file mode 100644 index 0000000000..275a26c7c0 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsMessageDeliveryException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 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.web.socket.sockjs; + +import java.util.List; + + +/** + * An exception thrown when a message frame was successfully received over an HTTP POST + * and parsed but one or more of the messages it contained could not be delivered to the + * WebSocketHandler either because the handler failed or because the connection got + * closed. + *

+ * The SockJS session is not automatically closed after this exception. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@SuppressWarnings("serial") +public class SockJsMessageDeliveryException extends SockJsException { + + private final List undeliveredMessages; + + + public SockJsMessageDeliveryException(String sessionId, List undeliveredMessages, Throwable cause) { + super("Failed to deliver message(s) " + undeliveredMessages + " for session " + sessionId, sessionId, cause); + this.undeliveredMessages = undeliveredMessages; + } + + public List getUndeliveredMessages() { + return this.undeliveredMessages; + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java index 0430606c60..41d91c5ce8 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsService.java @@ -16,14 +16,24 @@ package org.springframework.web.socket.sockjs; -import java.io.IOException; - import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator; /** - * A service for processing SockJS HTTP requests. + * The main entry point for processing HTTP requests from SockJS clients. + *

+ * In a Servlet 3+ container, {@link SockJsHttpRequestHandler} can be used to invoke this + * service. The processing servlet, as well as all filters involved, must have + * asynchronous support enabled through the ServletContext API or by adding an + * {@code true} element to servlet and filter declarations + * in web.xml. + *

+ * The service can be integrated into any HTTP request handling mechanism (e.g. plain + * Servlet, Spring MVC, or other). It is expected that it will be mapped, as expected + * by the SockJS protocol, to a specific prefix (e.g. "/echo") including all sub-URLs + * (i.e. Ant path pattern "/echo/**"). * * @author Rossen Stoyanchev * @since 4.0 @@ -34,16 +44,27 @@ public interface SockJsService { /** - * Process a SockJS request. + * Process a SockJS HTTP request. + *

+ * See the "Base URL", "Static URLs", and "Session URLs" sections of the SockJS + * protocol for details on the types of URLs expected. * * @param request the current request * @param response the current response - * @param handler the handler to process messages with + * @param handler the handler that will exchange messages with the SockJS client * - * @throws IOException raised if writing the to response of the current request fails - * @throws SockJsProcessingException + * @throws SockJsException raised when request processing fails; generally, failed + * attempts to send messages to clients automatically close the SockJS session + * and raise {@link SockJsTransportFailureException}; failed attempts to read + * messages from clients do not automatically close the session and may result + * in {@link SockJsMessageDeliveryException} or {@link SockJsException}; + * exceptions from the WebSocketHandler can be handled internally or through + * {@link ExceptionWebSocketHandlerDecorator} or some alternative decorator. + * The former is automatically added when using + * {@link SockJsHttpRequestHandler}. */ void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) - throws IOException, SockJsProcessingException; + throws SockJsException; } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsTransportFailureException.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsTransportFailureException.java new file mode 100644 index 0000000000..55342e976b --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsTransportFailureException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2013 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.web.socket.sockjs; + + +/** + * Indicates a serious failure that occurred in the SockJS implementation as opposed to in + * user code (e.g. IOException while writing to the response). When this exception is + * raised, the SockJS session is typically closed. + * + * @author Rossen Stoyanchev + * @since 4.0 + */ +@SuppressWarnings("serial") +public class SockJsTransportFailureException extends SockJsException { + + + public SockJsTransportFailureException(String message, String sessionId, Throwable cause) { + super(message, sessionId, cause); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java index b807556012..44597106f9 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/AbstractSockJsService.java @@ -45,23 +45,21 @@ import org.springframework.util.DigestUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.sockjs.SockJsProcessingException; +import org.springframework.web.socket.sockjs.SockJsException; import org.springframework.web.socket.sockjs.SockJsService; /** * An abstract base class for {@link SockJsService} implementations that provides SockJS * path resolution and handling of static SockJS requests (e.g. "/info", "/iframe.html", - * etc). Transport-specific requests are left as abstract methods. + * etc). Sub-classes must handle session URLs (i.e. transport-specific requests). *

- * This service can be integrated into any HTTP request handling mechanism (e.g. plain - * Servlet, Spring MVC, or other). It is expected that it will be mapped correctly to a - * prefix (e.g. "/echo") and will also handle all sub-URLs (i.e. "/echo/**"). - *

- * The service itself is unaware of the underlying mapping mechanism but nevertheless must - * be able to extract the SockJS path, i.e. the portion of the request path following the - * prefix. In most cases, this class can auto-detect the SockJS path but it is also - * possible to configure explicitly the prefixes via - * {@link #setValidSockJsPrefixes(String...)}. + * This service is unaware of the underlying HTTP request processing mechanism and URL + * mappings but nevertheless needs to know the "SockJS path" for a given request, i.e. the + * portion of the URL path that follows the SockJS prefix. In most cases, this can be + * auto-detected since the SockJS + * client sends a "greeting URL" first. However it is recommended to configure + * explicitly the expected SockJS prefixes via {@link #setValidSockJsPrefixes(String...)} + * to eliminate any potential issues. * * @author Rossen Stoyanchev * @since 4.0 @@ -265,11 +263,14 @@ public abstract class AbstractSockJsService implements SockJsService { } /** - * TODO + * {@inheritDoc} + *

+ * This method determines the SockJS path and handles SockJS static URLs. Session URLs + * and raw WebSocket requests are delegated to abstract methods. */ @Override public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler) - throws IOException, SockJsProcessingException { + throws SockJsException { String sockJsPath = getSockJsPath(request); if (sockJsPath == null) { @@ -292,42 +293,39 @@ public abstract class AbstractSockJsService implements SockJsService { if (sockJsPath.equals("") || sockJsPath.equals("/")) { response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8"))); response.getBody().write("Welcome to SockJS!\n".getBytes("UTF-8")); - return; } else if (sockJsPath.equals("/info")) { this.infoHandler.handle(request, response); - return; } else if (sockJsPath.matches("/iframe[0-9-.a-z_]*.html")) { this.iframeHandler.handle(request, response); - return; } else if (sockJsPath.equals("/websocket")) { handleRawWebSocketRequest(request, response, handler); - return; + } + else { + String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/"); + if (pathSegments.length != 3) { + logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\""); + response.setStatusCode(HttpStatus.NOT_FOUND); + return; + } + String serverId = pathSegments[0]; + String sessionId = pathSegments[1]; + String transport = pathSegments[2]; + + if (!validateRequest(serverId, sessionId, transport)) { + response.setStatusCode(HttpStatus.NOT_FOUND); + return; + } + handleTransportRequest(request, response, handler, sessionId, transport); } - String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/"); - if (pathSegments.length != 3) { - logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\""); - response.setStatusCode(HttpStatus.NOT_FOUND); - return; - } - - String serverId = pathSegments[0]; - String sessionId = pathSegments[1]; - String transport = pathSegments[2]; - - if (!validateRequest(serverId, sessionId, transport)) { - response.setStatusCode(HttpStatus.NOT_FOUND); - return; - } - - handleTransportRequest(request, response, handler, sessionId, transport); - } - finally { response.flush(); } + catch (IOException ex) { + throw new SockJsException("Failed to write to the response", null, ex); + } } /** @@ -385,14 +383,23 @@ public abstract class AbstractSockJsService implements SockJsService { return null; } + /** + * Validate whether the given transport String extracted from the URL is a valid + * SockJS transport type (regardless of whether a transport handler is configured). + */ protected abstract boolean isValidTransportType(String transportType); + /** + * Handle request for raw WebSocket communication, i.e. without any SockJS message framing. + */ protected abstract void handleRawWebSocketRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler) throws IOException; + /** + * Handle a SockJS session URL (i.e. transport-specific request). + */ protected abstract void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler webSocketHandler, String sessionId, String transport) - throws IOException, SockJsProcessingException; + WebSocketHandler webSocketHandler, String sessionId, String transport) throws SockJsException; protected boolean validateRequest(String serverId, String sessionId, String transport) { @@ -446,7 +453,7 @@ public abstract class AbstractSockJsService implements SockJsService { response.getHeaders().setCacheControl("no-store, no-cache, must-revalidate, max-age=0"); } - protected void sendMethodNotAllowed(ServerHttpResponse response, List httpMethods) throws IOException { + protected void sendMethodNotAllowed(ServerHttpResponse response, List httpMethods) { logger.debug("Sending Method Not Allowed (405)"); response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED); response.getHeaders().setAllow(new HashSet(httpMethods)); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsFrame.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsFrame.java index 66389c6353..fe51bdc3d5 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsFrame.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsFrame.java @@ -21,8 +21,7 @@ import java.nio.charset.Charset; import org.springframework.util.Assert; /** - * Represents a SockJS frames. Provides methods for access to commonly used message - * frames. + * Represents a SockJS frame and provides factory methods for creating SockJS frames. * * @author Rossen Stoyanchev * @since 4.0 diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsMessageCodec.java index c20324679f..4c558432da 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsMessageCodec.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/frame/SockJsMessageCodec.java @@ -21,8 +21,8 @@ import java.io.InputStream; /** - * A contract for encoding and decoding of messages to and from a SockJS message frame, - * which is essentially an array of JSON-encoded messages. For example: + * Encode and decode messages to and from a SockJS message frame, essentially an array of + * JSON-encoded messages. For example: * *

  * a["message1","message2"]
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandler.java
index 4a6ed8e295..a7ecd15b3a 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandler.java
@@ -20,19 +20,36 @@ import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
+import org.springframework.web.socket.sockjs.SockJsService;
 
 /**
- * Handles SockJS session URLs.
+ * Handle a SockJS session URL, i.e. transport-specific request.
  *
  * @author Rossen Stoyanchev
  * @since 4.0
  */
 public interface TransportHandler {
 
+
+	/**
+	 * @return the transport type supported by this handler
+	 */
 	TransportType getTransportType();
 
+	/**
+	 * Handle the given request and delegate messages to the provided
+	 * {@link WebSocketHandler}.
+	 *
+	 * @param request the current request
+	 * @param response the current response
+	 * @param handler the target WebSocketHandler, never {@code null}
+	 * @param session the SockJS session, never {@code null}
+	 *
+	 * @throws SockJsException raised when request processing fails as explained in
+	 *         {@link SockJsService}
+	 */
 	void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler handler, WebSocketSession session) throws SockJsProcessingException;
+			WebSocketHandler handler, WebSocketSession session) throws SockJsException;
 
 }
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportType.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportType.java
index 1a17c7f780..adc30b3e9c 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportType.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportType.java
@@ -25,7 +25,7 @@ import java.util.Map;
 import org.springframework.http.HttpMethod;
 
 /**
- * Defines SockJS transport types.
+ * SockJS transport types.
  *
  * @author Rossen Stoyanchev
  * @since 4.0
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpReceivingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpReceivingTransportHandler.java
index 930a536205..04c68e3764 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpReceivingTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpReceivingTransportHandler.java
@@ -25,17 +25,16 @@ import org.springframework.http.MediaType;
 import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.util.Assert;
-import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
 import org.springframework.web.socket.sockjs.transport.TransportHandler;
-import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
+import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession;
 
 import com.fasterxml.jackson.databind.JsonMappingException;
 
 /**
- * Base class for HTTP-based transports that read input messages from HTTP requests.
+ * Base class for HTTP transport handlers that receive messages via HTTP POST.
  *
  * @author Rossen Stoyanchev
  * @since 4.0
@@ -46,17 +45,19 @@ public abstract class AbstractHttpReceivingTransportHandler
 
 	@Override
 	public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsProcessingException {
+			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsException {
 
 		// TODO: check "Sec-WebSocket-Protocol" header
 		// https://github.com/sockjs/sockjs-client/issues/130
 
 		Assert.notNull(wsSession, "No session");
-		handleRequestInternal(request, response, wsHandler, wsSession);
+		AbstractHttpSockJsSession sockJsSession = (AbstractHttpSockJsSession) wsSession;
+
+		handleRequestInternal(request, response, wsHandler, sockJsSession);
 	}
 
 	protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsProcessingException {
+			WebSocketHandler wsHandler, AbstractHttpSockJsSession sockJsSession) throws SockJsException {
 
 		String[] messages = null;
 		try {
@@ -64,22 +65,22 @@ public abstract class AbstractHttpReceivingTransportHandler
 		}
 		catch (JsonMappingException ex) {
 			logger.error("Failed to read message: " + ex.getMessage());
-			sendInternalServerError(response, "Payload expected.", wsSession.getId());
+			handleReadError(response, "Payload expected.", sockJsSession.getId());
 			return;
 		}
 		catch (IOException ex) {
 			logger.error("Failed to read message: " + ex.getMessage());
-			sendInternalServerError(response, "Broken JSON encoding.", wsSession.getId());
+			handleReadError(response, "Broken JSON encoding.", sockJsSession.getId());
 			return;
 		}
 		catch (Throwable t) {
 			logger.error("Failed to read message: " + t.getMessage());
-			sendInternalServerError(response, "Failed to process messages", wsSession.getId());
+			handleReadError(response, "Failed to read message(s)", sockJsSession.getId());
 			return;
 		}
 
 		if (messages == null) {
-			sendInternalServerError(response, "Payload expected.", wsSession.getId());
+			handleReadError(response, "Payload expected.", sockJsSession.getId());
 			return;
 		}
 
@@ -90,26 +91,16 @@ public abstract class AbstractHttpReceivingTransportHandler
 		response.setStatusCode(getResponseStatus());
 		response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8")));
 
-		try {
-			for (String message : messages) {
-				wsHandler.handleMessage(wsSession, new TextMessage(message));
-			}
-		}
-		catch (Throwable t) {
-			ExceptionWebSocketHandlerDecorator.tryCloseWithError(wsSession, t, logger);
-			throw new SockJsProcessingException("Unhandled WebSocketHandler error in " + this, t, wsSession.getId());
-		}
+		sockJsSession.delegateMessages(messages);
 	}
 
-	protected void sendInternalServerError(ServerHttpResponse response, String error,
-			String sessionId) throws SockJsProcessingException {
-
+	private void handleReadError(ServerHttpResponse resp, String error, String sessionId) {
 		try {
-			response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
-			response.getBody().write(error.getBytes("UTF-8"));
+			resp.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
+			resp.getBody().write(error.getBytes("UTF-8"));
 		}
-		catch (Throwable t) {
-			throw new SockJsProcessingException("Failed to send error message to client", t, sessionId);
+		catch (IOException ex) {
+			throw new SockJsException("Failed to send error: " + error, sessionId, ex);
 		}
 	}
 
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java
index ccdb07bbff..52e0b81a5c 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/AbstractHttpSendingTransportHandler.java
@@ -23,14 +23,14 @@ import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.FrameFormat;
 import org.springframework.web.socket.sockjs.transport.TransportHandler;
 import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession;
 
 /**
- * Base class for HTTP-based transports that send messages over HTTP.
+ * Base class for HTTP transport handlers that push messages to connected clients.
  *
  * @author Rossen Stoyanchev
  * @since 4.0
@@ -41,17 +41,17 @@ public abstract class AbstractHttpSendingTransportHandler extends TransportHandl
 
 	@Override
 	public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler webSocketHandler, WebSocketSession session) throws SockJsProcessingException {
+			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsException {
 
 		// Set content type before writing
 		response.getHeaders().setContentType(getContentType());
 
-		AbstractHttpSockJsSession sockJsSession = (AbstractHttpSockJsSession) session;
+		AbstractHttpSockJsSession sockJsSession = (AbstractHttpSockJsSession) wsSession;
 		handleRequestInternal(request, response, sockJsSession);
 	}
 
 	protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
-			AbstractHttpSockJsSession sockJsSession) throws SockJsProcessingException {
+			AbstractHttpSockJsSession sockJsSession) throws SockJsException {
 
 		if (sockJsSession.isNew()) {
 			logger.debug("Opening " + getTransportType() + " connection");
@@ -62,13 +62,13 @@ public abstract class AbstractHttpSendingTransportHandler extends TransportHandl
 			sockJsSession.setLongPollingRequest(request, response, getFrameFormat(request));
 		}
 		else {
+			logger.debug("another " + getTransportType() + " connection still open: " + sockJsSession);
+			SockJsFrame frame = getFrameFormat(request).format(SockJsFrame.closeFrameAnotherConnectionOpen());
 			try {
-				logger.debug("another " + getTransportType() + " connection still open: " + sockJsSession);
-				SockJsFrame closeFrame = SockJsFrame.closeFrameAnotherConnectionOpen();
-				response.getBody().write(getFrameFormat(request).format(closeFrame).getContentBytes());
+				response.getBody().write(frame.getContentBytes());
 			}
-			catch (IOException e) {
-				throw new SockJsProcessingException("Failed to send SockJS close frame", e, sockJsSession.getId());
+			catch (IOException ex) {
+				throw new SockJsException("Failed to send " + frame, sockJsSession.getId(), ex);
 			}
 		}
 	}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/DefaultSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/DefaultSockJsService.java
index 4c01c0b417..04166fa016 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/DefaultSockJsService.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/DefaultSockJsService.java
@@ -43,7 +43,7 @@ import org.springframework.web.socket.WebSocketSession;
 import org.springframework.web.socket.server.DefaultHandshakeHandler;
 import org.springframework.web.socket.server.HandshakeHandler;
 import org.springframework.web.socket.server.support.ServerWebSocketSessionInitializer;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
 import org.springframework.web.socket.sockjs.SockJsService;
 import org.springframework.web.socket.sockjs.support.AbstractSockJsService;
 import org.springframework.web.socket.sockjs.support.frame.Jackson2SockJsMessageCodec;
@@ -203,8 +203,7 @@ public class DefaultSockJsService extends AbstractSockJsService {
 
 	@Override
 	protected void handleTransportRequest(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler wsHandler, String sessionId, String transport)
-					throws IOException, SockJsProcessingException {
+			WebSocketHandler wsHandler, String sessionId, String transport) throws SockJsException {
 
 		TransportType transportType = TransportType.fromValue(transport);
 		if (transportType == null) {
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java
index 72606b2081..e09df75994 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/HtmlFileTransportHandler.java
@@ -24,7 +24,9 @@ import org.springframework.http.MediaType;
 import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.util.StringUtils;
+import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.DefaultFrameFormat;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.FrameFormat;
 import org.springframework.web.socket.sockjs.transport.TransportHandler;
@@ -32,7 +34,6 @@ import org.springframework.web.socket.sockjs.transport.TransportType;
 import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession;
 import org.springframework.web.socket.sockjs.transport.session.SockJsServiceConfig;
 import org.springframework.web.socket.sockjs.transport.session.StreamingSockJsSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
 import org.springframework.web.util.JavaScriptUtils;
 
 /**
@@ -92,20 +93,22 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
 
 	@Override
 	public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
-			AbstractHttpSockJsSession session) throws SockJsProcessingException {
+			AbstractHttpSockJsSession sockJsSession) {
 
-		try {
 			String callback = request.getQueryParams().getFirst("c");
 			if (! StringUtils.hasText(callback)) {
 				response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
-				response.getBody().write("\"callback\" parameter required".getBytes("UTF-8"));
+				try {
+					response.getBody().write("\"callback\" parameter required".getBytes("UTF-8"));
+				}
+				catch (IOException t) {
+					sockJsSession.tryCloseWithSockJsTransportError(t, CloseStatus.SERVER_ERROR);
+					throw new SockJsTransportFailureException("Failed to write to response", sockJsSession.getId(), t);
+				}
 				return;
 			}
-		}
-		catch (Throwable t) {
-			throw new SockJsProcessingException("Failed to send error to client", t, session.getId());
-		}
-		super.handleRequestInternal(request, response, session);
+
+		super.handleRequestInternal(request, response, sockJsSession);
 	}
 
 	@Override
@@ -127,7 +130,8 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle
 
 		@Override
 		protected void writePrelude() throws IOException {
-			// we already validated the parameter..
+
+			// we already validated the parameter above..
 			String callback = getRequest().getQueryParams().getFirst("c");
 
 			String html = String.format(PARTIAL_HTML_CONTENT, callback);
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpPollingTransportHandler.java
index ea8e81bd85..0d8cea2dde 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpPollingTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpPollingTransportHandler.java
@@ -23,13 +23,15 @@ import org.springframework.http.MediaType;
 import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.util.StringUtils;
+import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.FrameFormat;
 import org.springframework.web.socket.sockjs.transport.TransportType;
 import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession;
 import org.springframework.web.socket.sockjs.transport.session.PollingSockJsSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
 import org.springframework.web.util.JavaScriptUtils;
 
 /**
@@ -57,7 +59,7 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa
 
 	@Override
 	public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
-			AbstractHttpSockJsSession session) throws SockJsProcessingException {
+			AbstractHttpSockJsSession sockJsSession) throws SockJsException {
 
 		try {
 			String callback = request.getQueryParams().getFirst("c");
@@ -68,16 +70,17 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa
 			}
 		}
 		catch (Throwable t) {
-			throw new SockJsProcessingException("Failed to send error to client", t, session.getId());
+			sockJsSession.tryCloseWithSockJsTransportError(t, CloseStatus.SERVER_ERROR);
+			throw new SockJsTransportFailureException("Failed to send error", sockJsSession.getId(), t);
 		}
 
-		super.handleRequestInternal(request, response, session);
+		super.handleRequestInternal(request, response, sockJsSession);
 	}
 
 	@Override
 	protected FrameFormat getFrameFormat(ServerHttpRequest request) {
 
-		// we already validated the parameter..
+		// we already validated the parameter above..
 		String callback = request.getQueryParams().getFirst("c");
 
 		return new SockJsFrame.DefaultFrameFormat(callback + "(\"%s\");\r\n") {
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpTransportHandler.java
index eafc0f84eb..6709c46027 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/JsonpTransportHandler.java
@@ -26,11 +26,11 @@ import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.util.MultiValueMap;
 import org.springframework.util.StringUtils;
 import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsMessageCodec;
 import org.springframework.web.socket.sockjs.transport.TransportHandler;
 import org.springframework.web.socket.sockjs.transport.TransportType;
+import org.springframework.web.socket.sockjs.transport.session.AbstractHttpSockJsSession;
 
 /**
  * A {@link TransportHandler} that receives messages over HTTP.
@@ -49,15 +49,14 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler
 
 	@Override
 	public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler webSocketHandler, WebSocketSession webSocketSession) throws SockJsProcessingException {
-
-		super.handleRequestInternal(request, response, webSocketHandler, webSocketSession);
+			WebSocketHandler wsHandler, AbstractHttpSockJsSession sockJsSession) throws SockJsException {
 
+		super.handleRequestInternal(request, response, wsHandler, sockJsSession);
 		try {
 			response.getBody().write("ok".getBytes("UTF-8"));
 		}
-		catch (Throwable t) {
-			throw new SockJsProcessingException("Failed to write response body", t, webSocketSession.getId());
+		catch(IOException ex) {
+			throw new SockJsException("Failed to write to the response body", sockJsSession.getId(), ex);
 		}
 	}
 
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/SockJsWebSocketHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/SockJsWebSocketHandler.java
index ef8a64b502..59b6f74ee4 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/SockJsWebSocketHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/SockJsWebSocketHandler.java
@@ -46,20 +46,20 @@ public class SockJsWebSocketHandler extends TextWebSocketHandlerAdapter {
 
 	private final SockJsServiceConfig sockJsServiceConfig;
 
-	private final WebSocketServerSockJsSession session;
+	private final WebSocketServerSockJsSession sockJsSession;
 
 	private final AtomicInteger sessionCount = new AtomicInteger(0);
 
 
-	public SockJsWebSocketHandler(SockJsServiceConfig sockJsServiceConfig,
-			WebSocketHandler webSocketHandler, WebSocketServerSockJsSession session) {
+	public SockJsWebSocketHandler(SockJsServiceConfig serviceConfig,
+			WebSocketHandler webSocketHandler, WebSocketServerSockJsSession sockJsSession) {
 
-		Assert.notNull(sockJsServiceConfig, "sockJsServiceConfig must not be null");
+		Assert.notNull(serviceConfig, "serviceConfig must not be null");
 		Assert.notNull(webSocketHandler, "webSocketHandler must not be null");
-		Assert.notNull(session, "session must not be null");
+		Assert.notNull(sockJsSession, "session must not be null");
 
-		this.sockJsServiceConfig = sockJsServiceConfig;
-		this.session = session;
+		this.sockJsServiceConfig = serviceConfig;
+		this.sockJsSession = sockJsSession;
 	}
 
 	protected SockJsServiceConfig getSockJsConfig() {
@@ -69,22 +69,22 @@ public class SockJsWebSocketHandler extends TextWebSocketHandlerAdapter {
 	@Override
 	public void afterConnectionEstablished(WebSocketSession wsSession) throws Exception {
 		Assert.isTrue(this.sessionCount.compareAndSet(0, 1), "Unexpected connection");
-		this.session.initWebSocketSession(wsSession);
+		this.sockJsSession.initWebSocketSession(wsSession);
 	}
 
 	@Override
 	public void handleTextMessage(WebSocketSession wsSession, TextMessage message) throws Exception {
-		this.session.handleMessage(message, wsSession);
+		this.sockJsSession.handleMessage(message, wsSession);
 	}
 
 	@Override
 	public void afterConnectionClosed(WebSocketSession wsSession, CloseStatus status) throws Exception {
-		this.session.delegateConnectionClosed(status);
+		this.sockJsSession.delegateConnectionClosed(status);
 	}
 
 	@Override
 	public void handleTransportError(WebSocketSession webSocketSession, Throwable exception) throws Exception {
-		this.session.delegateError(exception);
+		this.sockJsSession.delegateError(exception);
 	}
 
 }
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java
index 7bb2b1c8b5..f6d40b33b3 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/handler/WebSocketTransportHandler.java
@@ -21,10 +21,12 @@ import java.io.IOException;
 import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.util.Assert;
+import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketSession;
 import org.springframework.web.socket.server.HandshakeHandler;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.transport.TransportHandler;
 import org.springframework.web.socket.sockjs.transport.TransportType;
 import org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession;
@@ -64,15 +66,16 @@ public class WebSocketTransportHandler extends TransportHandlerSupport
 
 	@Override
 	public void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
-			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsProcessingException {
+			WebSocketHandler wsHandler, WebSocketSession wsSession) throws SockJsException {
 
+		WebSocketServerSockJsSession sockJsSession = (WebSocketServerSockJsSession) wsSession;
 		try {
-			WebSocketServerSockJsSession sockJsSession = (WebSocketServerSockJsSession) wsSession;
-			WebSocketHandler sockJsHandler = new SockJsWebSocketHandler(getSockJsServiceConfig(), wsHandler, sockJsSession);
-			this.handshakeHandler.doHandshake(request, response, sockJsHandler);
+			wsHandler = new SockJsWebSocketHandler(getSockJsServiceConfig(), wsHandler, sockJsSession);
+			this.handshakeHandler.doHandshake(request, response, wsHandler);
 		}
 		catch (Throwable t) {
-			throw new SockJsProcessingException("Failed to start handshake request", t, wsSession.getId());
+			sockJsSession.tryCloseWithSockJsTransportError(t, CloseStatus.SERVER_ERROR);
+			throw new SockJsTransportFailureException("WebSocket handshake failure", wsSession.getId(), t);
 		}
 	}
 
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java
index d41b06f279..0cca3c8a00 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSession.java
@@ -27,10 +27,10 @@ import org.springframework.http.server.ServletServerHttpAsyncRequestControl;
 import org.springframework.util.Assert;
 import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.FrameFormat;
-import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
 
 /**
  * An abstract base class for use with HTTP transport based SockJS sessions.
@@ -77,22 +77,22 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
 	}
 
 	public synchronized void setInitialRequest(ServerHttpRequest request, ServerHttpResponse response,
-			FrameFormat frameFormat) throws SockJsProcessingException {
+			FrameFormat frameFormat) throws SockJsException {
 
+		udpateRequest(request, response, frameFormat);
 		try {
-			udpateRequest(request, response, frameFormat);
 			writePrelude();
 			writeFrame(SockJsFrame.openFrame());
 		}
 		catch (Throwable t) {
-			tryCloseWithSockJsTransportError(t, null);
-			throw new SockJsProcessingException("Failed open SockJS session", t, getId());
+			tryCloseWithSockJsTransportError(t, CloseStatus.SERVER_ERROR);
+			throw new SockJsTransportFailureException("Failed to send \"open\" frame", getId(), t);
 		}
 		try {
 			delegateConnectionEstablished();
 		}
 		catch (Throwable t) {
-			ExceptionWebSocketHandlerDecorator.tryCloseWithError(this, t, logger);
+			throw new SockJsException("Unhandled exception from WebSocketHandler", getId(), t);
 		}
 	}
 
@@ -100,30 +100,22 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
 	}
 
 	public synchronized void setLongPollingRequest(ServerHttpRequest request, ServerHttpResponse response,
-			FrameFormat frameFormat) throws SockJsProcessingException {
+			FrameFormat frameFormat) throws SockJsException {
 
+		udpateRequest(request, response, frameFormat);
+		if (isClosed()) {
+			logger.debug("Connection already closed (but not removed yet)");
+			writeFrame(SockJsFrame.closeFrameGoAway());
+			return;
+		}
 		try {
-			udpateRequest(request, response, frameFormat);
-
-			if (isClosed()) {
-				logger.debug("connection already closed");
-				try {
-					writeFrame(SockJsFrame.closeFrameGoAway());
-				}
-				catch (IOException ex) {
-					throw new SockJsProcessingException("Failed to send SockJS close frame", ex, getId());
-				}
-				return;
-			}
-
 			this.asyncControl.start(-1);
-
 			scheduleHeartbeat();
 			tryFlushCache();
 		}
 		catch (Throwable t) {
-			tryCloseWithSockJsTransportError(t, null);
-			throw new SockJsProcessingException("Failed to start long running request and flush messages", t, getId());
+			tryCloseWithSockJsTransportError(t, CloseStatus.SERVER_ERROR);
+			throw new SockJsTransportFailureException("Failed to flush messages", getId(), t);
 		}
 	}
 
@@ -156,12 +148,12 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
 	}
 
 	@Override
-	protected final synchronized void sendMessageInternal(String message) throws IOException {
+	protected final synchronized void sendMessageInternal(String message) throws SockJsTransportFailureException {
 		this.messageCache.add(message);
 		tryFlushCache();
 	}
 
-	private void tryFlushCache() throws IOException {
+	private void tryFlushCache() throws SockJsTransportFailureException {
 		if (isActive() && !getMessageCache().isEmpty()) {
 			logger.trace("Flushing messages");
 			flushCache();
@@ -171,7 +163,7 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
 	/**
 	 * Only called if the connection is currently active
 	 */
-	protected abstract void flushCache() throws IOException;
+	protected abstract void flushCache() throws SockJsTransportFailureException;
 
 	@Override
 	protected void disconnect(CloseStatus status) {
@@ -182,11 +174,11 @@ public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession {
 		updateLastActiveTime();
 		if (isActive() && this.asyncControl.hasStarted()) {
 			try {
-				logger.debug("Completing async request");
+				logger.debug("Completing asynchronous request");
 				this.asyncControl.complete();
 			}
 			catch (Throwable ex) {
-				logger.error("Failed to complete async request: " + ex.getMessage());
+				logger.error("Failed to complete request: " + ex.getMessage());
 			}
 		}
 		this.request = null;
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 f04f27f76c..6de66c5a9b 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
@@ -21,7 +21,10 @@ import java.io.IOException;
 import java.net.SocketException;
 import java.net.URI;
 import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
+import java.util.List;
 import java.util.concurrent.ScheduledFuture;
 
 import org.apache.commons.logging.Log;
@@ -33,7 +36,8 @@ import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketMessage;
 import org.springframework.web.socket.WebSocketSession;
 import org.springframework.web.socket.adapter.ConfigurableWebSocketSession;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsMessageDeliveryException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 
 /**
@@ -46,7 +50,6 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
 
 	protected final Log logger = LogFactory.getLog(getClass());
 
-
 	private final String id;
 
 	private URI uri;
@@ -185,9 +188,21 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
 		this.handler.afterConnectionEstablished(this);
 	}
 
-	public void delegateMessages(String[] messages) throws Exception {
+	public void delegateMessages(String[] messages) throws SockJsMessageDeliveryException {
+		List undelivered = new ArrayList(Arrays.asList(messages));
 		for (String message : messages) {
-			this.handler.handleMessage(this, new TextMessage(message));
+			try {
+				if (isClosed()) {
+					throw new SockJsMessageDeliveryException(this.id, undelivered, null);
+				}
+				else {
+					this.handler.handleMessage(this, new TextMessage(message));
+					undelivered.remove(0);
+				}
+			}
+			catch (Throwable t) {
+				throw new SockJsMessageDeliveryException(this.id, undelivered, t);
+			}
 		}
 	}
 
@@ -278,55 +293,53 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
 	/**
 	 * Close due to error arising from SockJS transport handling.
 	 */
-	protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) {
-		logger.error("Closing due to transport error for " + this, ex);
+	public void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) {
+		logger.error("Closing due to transport error for " + this);
 		try {
 			delegateError(ex);
 		}
 		catch (Throwable delegateEx) {
-			logger.error("Unhandled error for " + this, delegateEx);
 			try {
 				close(closeStatus);
 			}
 			catch (Throwable closeEx) {
-				logger.error("Unhandled error for " + this, closeEx);
+				// ignore
 			}
 		}
 	}
 
 	/**
 	 * For internal use within a TransportHandler and the (TransportHandler-specific)
-	 * session sub-class.
+	 * session class.
 	 */
-	protected void writeFrame(SockJsFrame frame) throws IOException {
+	protected void writeFrame(SockJsFrame frame) throws SockJsTransportFailureException {
 		if (logger.isTraceEnabled()) {
 			logger.trace("Preparing to write " + frame);
 		}
 		try {
 			writeFrameInternal(frame);
 		}
-		catch (IOException ex) {
+		catch (Throwable ex) {
 			if (ex instanceof EOFException || ex instanceof SocketException) {
 				logger.warn("Client went away. Terminating connection");
 			}
 			else {
-				logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
+				logger.warn("Terminating connection after failure to send message: " + ex.getMessage());
 			}
-			disconnect(CloseStatus.SERVER_ERROR);
-			close(CloseStatus.SERVER_ERROR);
-			throw ex;
-		}
-		catch (Throwable ex) {
-			logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
-			disconnect(CloseStatus.SERVER_ERROR);
-			close(CloseStatus.SERVER_ERROR);
-			throw new SockJsProcessingException("Failed to write " + frame, ex, this.getId());
+			try {
+				disconnect(CloseStatus.SERVER_ERROR);
+				close(CloseStatus.SERVER_ERROR);
+			}
+			catch (Throwable ex2) {
+				// ignore
+			}
+			throw new SockJsTransportFailureException("Failed to write " + frame, this.getId(), ex);
 		}
 	}
 
-	protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception;
+	protected abstract void writeFrameInternal(SockJsFrame frame) throws IOException;
 
-	public synchronized void sendHeartbeat() throws Exception {
+	public synchronized void sendHeartbeat() throws SockJsTransportFailureException {
 		if (isActive()) {
 			writeFrame(SockJsFrame.heartbeatFrame());
 			scheduleHeartbeat();
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/PollingSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/PollingSockJsSession.java
index 8de55152d8..9fe6a5b8c7 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/PollingSockJsSession.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/PollingSockJsSession.java
@@ -16,9 +16,8 @@
 
 package org.springframework.web.socket.sockjs.transport.session;
 
-import java.io.IOException;
-
 import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 import org.springframework.web.socket.sockjs.support.frame.SockJsMessageCodec;
 
@@ -35,7 +34,7 @@ public class PollingSockJsSession extends AbstractHttpSockJsSession {
 
 
 	@Override
-	protected void flushCache() throws IOException {
+	protected void flushCache() throws SockJsTransportFailureException {
 
 		cancelHeartbeat();
 		String[] messages = getMessageCache().toArray(new String[getMessageCache().size()]);
@@ -47,7 +46,7 @@ public class PollingSockJsSession extends AbstractHttpSockJsSession {
 	}
 
 	@Override
-	protected void writeFrame(SockJsFrame frame) throws IOException {
+	protected void writeFrame(SockJsFrame frame) throws SockJsTransportFailureException {
 		super.writeFrame(frame);
 		resetRequest();
 	}
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/StreamingSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/StreamingSockJsSession.java
index 2cf2813d9d..7ae5770e44 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/StreamingSockJsSession.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/StreamingSockJsSession.java
@@ -21,10 +21,11 @@ import java.io.IOException;
 import org.springframework.http.server.ServerHttpRequest;
 import org.springframework.http.server.ServerHttpResponse;
 import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
-import org.springframework.web.socket.sockjs.support.frame.SockJsMessageCodec;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame.FrameFormat;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.support.frame.SockJsMessageCodec;
 
 /**
  * A SockJS session for use with streaming HTTP transports.
@@ -43,7 +44,7 @@ public class StreamingSockJsSession extends AbstractHttpSockJsSession {
 
 	@Override
 	public synchronized void setInitialRequest(ServerHttpRequest request, ServerHttpResponse response,
-			FrameFormat frameFormat) throws SockJsProcessingException {
+			FrameFormat frameFormat) throws SockJsException {
 
 		super.setInitialRequest(request, response, frameFormat);
 
@@ -54,7 +55,7 @@ public class StreamingSockJsSession extends AbstractHttpSockJsSession {
 	}
 
 	@Override
-	protected void flushCache() throws IOException {
+	protected void flushCache() throws SockJsTransportFailureException {
 
 		cancelHeartbeat();
 
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSession.java
index 5d2f8f34e1..bb2f48b80c 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSession.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSession.java
@@ -23,6 +23,7 @@ import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
 import org.springframework.web.socket.sockjs.support.frame.SockJsMessageCodec;
 
@@ -63,7 +64,7 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
 			this.webSocketSession.sendMessage(message);
 		}
 		catch (IOException ex) {
-			tryCloseWithSockJsTransportError(ex, null);
+			tryCloseWithSockJsTransportError(ex, CloseStatus.SERVER_ERROR);
 			return;
 		}
 		scheduleHeartbeat();
@@ -94,7 +95,7 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
 	}
 
 	@Override
-	public void sendMessageInternal(String message) throws IOException {
+	public void sendMessageInternal(String message) throws SockJsTransportFailureException {
 		cancelHeartbeat();
 		SockJsMessageCodec messageCodec = getSockJsServiceConfig().getMessageCodec();
 		SockJsFrame frame = SockJsFrame.messageFrame(messageCodec, message);
@@ -113,7 +114,9 @@ public class WebSocketServerSockJsSession extends AbstractSockJsSession {
 
 	@Override
 	protected void disconnect(CloseStatus status) throws IOException {
-		this.webSocketSession.close(status);
+		if (this.webSocketSession != null) {
+			this.webSocketSession.close(status);
+		}
 	}
 
 }
diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/support/LoggingWebSocketHandlerDecorator.java b/spring-websocket/src/main/java/org/springframework/web/socket/support/LoggingWebSocketHandlerDecorator.java
index d5b12fa9e7..e0eaa9bfa7 100644
--- a/spring-websocket/src/main/java/org/springframework/web/socket/support/LoggingWebSocketHandlerDecorator.java
+++ b/spring-websocket/src/main/java/org/springframework/web/socket/support/LoggingWebSocketHandlerDecorator.java
@@ -58,6 +58,9 @@ public class LoggingWebSocketHandlerDecorator extends WebSocketHandlerDecorator
 	@Override
 	public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
 		if (logger.isDebugEnabled()) {
+			logger.debug("Transport error for " + session + ": " + exception);
+		}
+		else if (logger.isTraceEnabled()) {
 			logger.debug("Transport error for " + session, exception);
 		}
 		super.handleTransportError(session, exception);
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/AbstractSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/AbstractSockJsServiceTests.java
index 2503f5fafa..8a859b2964 100644
--- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/AbstractSockJsServiceTests.java
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/AbstractSockJsServiceTests.java
@@ -27,7 +27,7 @@ import org.springframework.scheduling.TaskScheduler;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
 import org.springframework.web.socket.AbstractHttpRequestTests;
 import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsException;
 import org.springframework.web.socket.sockjs.transport.TransportType;
 
 import static org.junit.Assert.*;
@@ -246,7 +246,7 @@ public class AbstractSockJsServiceTests extends AbstractHttpRequestTests {
 
 		@Override
 		protected void handleTransportRequest(ServerHttpRequest req, ServerHttpResponse res, WebSocketHandler handler,
-				String sessionId, String transport) throws IOException, SockJsProcessingException {
+				String sessionId, String transport) throws IOException, SockJsException {
 
 			this.sessionId = sessionId;
 			this.transport = transport;
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpReceivingTransportHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpReceivingTransportHandlerTests.java
index 1fd4b45b0a..ac52e08728 100644
--- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpReceivingTransportHandlerTests.java
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/handler/HttpReceivingTransportHandlerTests.java
@@ -20,13 +20,12 @@ import org.junit.Before;
 import org.junit.Test;
 import org.springframework.http.MediaType;
 import org.springframework.web.socket.AbstractHttpRequestTests;
-import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.TextMessage;
 import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.sockjs.SockJsMessageDeliveryException;
 import org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession;
 import org.springframework.web.socket.sockjs.transport.session.StubSockJsServiceConfig;
-import org.springframework.web.socket.sockjs.transport.session.TestSockJsSession;
+import org.springframework.web.socket.sockjs.transport.session.TestHttpSockJsSession;
 
 import static org.junit.Assert.*;
 import static org.mockito.Mockito.*;
@@ -107,48 +106,47 @@ public class HttpReceivingTransportHandlerTests  extends AbstractHttpRequestTest
 
 		this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8"));
 
-		WebSocketHandler webSocketHandler = mock(WebSocketHandler.class);
-		TestSockJsSession session = new TestSockJsSession("1", sockJsConfig, webSocketHandler);
+		WebSocketHandler wsHandler = mock(WebSocketHandler.class);
+		TestHttpSockJsSession session = new TestHttpSockJsSession("1", sockJsConfig, wsHandler);
 		session.delegateConnectionEstablished();
 
-		doThrow(new Exception()).when(webSocketHandler).handleMessage(session, new TextMessage("x"));
+		doThrow(new Exception()).when(wsHandler).handleMessage(session, new TextMessage("x"));
 
 		try {
 			XhrTransportHandler transportHandler = new XhrTransportHandler();
 			transportHandler.setSockJsServiceConfiguration(sockJsConfig);
-			transportHandler.handleRequest(this.request, this.response, webSocketHandler, session);
+			transportHandler.handleRequest(this.request, this.response, wsHandler, session);
 			fail("Expected exception");
 		}
-		catch (SockJsProcessingException ex) {
-			assertEquals(CloseStatus.SERVER_ERROR, session.getStatus());
+		catch (SockJsMessageDeliveryException ex) {
+			assertNull(session.getCloseStatus());
 		}
 	}
 
 
-	private void handleRequest(AbstractHttpReceivingTransportHandler transportHandler)
-			throws Exception {
+	private void handleRequest(AbstractHttpReceivingTransportHandler transportHandler) throws Exception {
 
-		WebSocketHandler webSocketHandler = mock(WebSocketHandler.class);
-		AbstractSockJsSession session = new TestSockJsSession("1", new StubSockJsServiceConfig(), webSocketHandler);
+		WebSocketHandler wsHandler = mock(WebSocketHandler.class);
+		AbstractSockJsSession session = new TestHttpSockJsSession("1", new StubSockJsServiceConfig(), wsHandler);
 
 		transportHandler.setSockJsServiceConfiguration(new StubSockJsServiceConfig());
-		transportHandler.handleRequest(this.request, this.response, webSocketHandler, session);
+		transportHandler.handleRequest(this.request, this.response, wsHandler, session);
 
 		assertEquals("text/plain;charset=UTF-8", this.response.getHeaders().getContentType().toString());
-		verify(webSocketHandler).handleMessage(session, new TextMessage("x"));
+		verify(wsHandler).handleMessage(session, new TextMessage("x"));
 	}
 
 	private void handleRequestAndExpectFailure() throws Exception {
 
 		resetResponse();
 
-		WebSocketHandler webSocketHandler = mock(WebSocketHandler.class);
-		AbstractSockJsSession session = new TestSockJsSession("1", new StubSockJsServiceConfig(), webSocketHandler);
+		WebSocketHandler wsHandler = mock(WebSocketHandler.class);
+		AbstractSockJsSession session = new TestHttpSockJsSession("1", new StubSockJsServiceConfig(), wsHandler);
 
-		new XhrTransportHandler().handleRequest(this.request, this.response, webSocketHandler, session);
+		new XhrTransportHandler().handleRequest(this.request, this.response, wsHandler, session);
 
 		assertEquals(500, this.servletResponse.getStatus());
-		verifyNoMoreInteractions(webSocketHandler);
+		verifyNoMoreInteractions(wsHandler);
 	}
 
 }
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSessionTests.java
index b39f4ccf2d..74abb4fb31 100644
--- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSessionTests.java
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractHttpSockJsSessionTests.java
@@ -151,7 +151,7 @@ public class AbstractHttpSockJsSessionTests extends BaseAbstractSockJsSessionTes
 		}
 
 		@Override
-		protected void flushCache() throws IOException {
+		protected void flushCache() {
 			this.cacheFlushed = true;
 		}
 
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSessionTests.java
index 987f859195..d993960d39 100644
--- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSessionTests.java
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/AbstractSockJsSessionTests.java
@@ -18,15 +18,18 @@ package org.springframework.web.socket.sockjs.transport.session;
 
 import java.io.IOException;
 import java.sql.Date;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.concurrent.ScheduledFuture;
 
 import org.junit.Test;
 import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.TextMessage;
-import org.springframework.web.socket.sockjs.SockJsProcessingException;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.sockjs.SockJsMessageDeliveryException;
+import org.springframework.web.socket.sockjs.SockJsTransportFailureException;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
-import org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession;
+import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
 
 import static org.junit.Assert.*;
 import static org.mockito.Matchers.*;
@@ -95,6 +98,33 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests sockJsFrames = new ArrayList<>();
+
+	private CloseStatus closeStatus;
+
+	private IOException exceptionOnWrite;
+
+	private int numberOfLastActiveTimeUpdates;
+
+	private boolean cancelledHeartbeat;
+
+	private String subProtocol;
+
+
+	public TestHttpSockJsSession(String sessionId, SockJsServiceConfig config, WebSocketHandler handler) {
+		super(sessionId, config, handler);
+	}
+
+	@Override
+	public String getAcceptedProtocol() {
+		return this.subProtocol;
+	}
+
+	@Override
+	public void setAcceptedProtocol(String protocol) {
+		this.subProtocol = protocol;
+	}
+
+	public CloseStatus getCloseStatus() {
+		return this.closeStatus;
+	}
+
+	@Override
+	public boolean isActive() {
+		return this.active;
+	}
+
+	public void setActive(boolean active) {
+		this.active = active;
+	}
+
+	public List getSockJsFramesWritten() {
+		return this.sockJsFrames;
+	}
+
+	public void setExceptionOnWrite(IOException exceptionOnWrite) {
+		this.exceptionOnWrite = exceptionOnWrite;
+	}
+
+	public int getNumberOfLastActiveTimeUpdates() {
+		return this.numberOfLastActiveTimeUpdates;
+	}
+
+	public boolean didCancelHeartbeat() {
+		return this.cancelledHeartbeat;
+	}
+
+	@Override
+	protected void updateLastActiveTime() {
+		this.numberOfLastActiveTimeUpdates++;
+		super.updateLastActiveTime();
+	}
+
+	@Override
+	protected void cancelHeartbeat() {
+		this.cancelledHeartbeat = true;
+		super.cancelHeartbeat();
+	}
+
+	@Override
+	protected void writeFrameInternal(SockJsFrame frame) throws IOException {
+		this.sockJsFrames.add(frame);
+		if (this.exceptionOnWrite != null) {
+			throw this.exceptionOnWrite;
+		}
+	}
+
+	@Override
+	protected void disconnect(CloseStatus status) {
+		this.closeStatus = status;
+	}
+
+	@Override
+	protected void flushCache() throws SockJsTransportFailureException {
+	}
+
+}
diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/TestSockJsSession.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/TestSockJsSession.java
index bac5eba932..faf70ede1c 100644
--- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/TestSockJsSession.java
+++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/TestSockJsSession.java
@@ -23,7 +23,6 @@ import java.util.List;
 import org.springframework.web.socket.CloseStatus;
 import org.springframework.web.socket.WebSocketHandler;
 import org.springframework.web.socket.sockjs.support.frame.SockJsFrame;
-import org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession;
 
 /**
  * @author Rossen Stoyanchev
@@ -32,11 +31,11 @@ public class TestSockJsSession extends AbstractSockJsSession {
 
 	private boolean active;
 
-	private final List sockJsFramesWritten = new ArrayList<>();
+	private final List sockJsFrames = new ArrayList<>();
 
-	private CloseStatus status;
+	private CloseStatus closeStatus;
 
-	private Exception exceptionOnWriteFrame;
+	private IOException exceptionOnWrite;
 
 	private int numberOfLastActiveTimeUpdates;
 
@@ -59,8 +58,8 @@ public class TestSockJsSession extends AbstractSockJsSession {
 		this.subProtocol = protocol;
 	}
 
-	public CloseStatus getStatus() {
-		return this.status;
+	public CloseStatus getCloseStatus() {
+		return this.closeStatus;
 	}
 
 	@Override
@@ -73,11 +72,11 @@ public class TestSockJsSession extends AbstractSockJsSession {
 	}
 
 	public List getSockJsFramesWritten() {
-		return this.sockJsFramesWritten;
+		return this.sockJsFrames;
 	}
 
-	public void setExceptionOnWriteFrame(Exception exceptionOnWriteFrame) {
-		this.exceptionOnWriteFrame = exceptionOnWriteFrame;
+	public void setExceptionOnWrite(IOException exceptionOnWrite) {
+		this.exceptionOnWrite = exceptionOnWrite;
 	}
 
 	public int getNumberOfLastActiveTimeUpdates() {
@@ -101,20 +100,20 @@ public class TestSockJsSession extends AbstractSockJsSession {
 	}
 
 	@Override
-	protected void sendMessageInternal(String message) throws IOException {
+	protected void sendMessageInternal(String message) {
 	}
 
 	@Override
-	protected void writeFrameInternal(SockJsFrame frame) throws Exception {
-		this.sockJsFramesWritten.add(frame);
-		if (this.exceptionOnWriteFrame != null) {
-			throw this.exceptionOnWriteFrame;
+	protected void writeFrameInternal(SockJsFrame frame) throws IOException {
+		this.sockJsFrames.add(frame);
+		if (this.exceptionOnWrite != null) {
+			throw this.exceptionOnWrite;
 		}
 	}
 
 	@Override
 	protected void disconnect(CloseStatus status) throws IOException {
-		this.status = status;
+		this.closeStatus = status;
 	}
 
 }