Polish SockJS exception handling and javadoc

See javadoc in SockJsService for details.

Also remove ReadOnlyMultiValueMap, CollectionUtils has a method for
that already.
This commit is contained in:
Rossen Stoyanchev 2013-08-03 11:01:07 -04:00
parent a03517fa35
commit 15a2f03459
31 changed files with 537 additions and 421 deletions

View File

@ -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<K, V> implements MultiValueMap<K, V> {
private final MultiValueMap<K, V> targetMap;
/**
* Create a new ReadOnlyMultiValueMap that wraps the given target map.
*/
public ReadOnlyMultiValueMap(MultiValueMap<K, V> 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<K, V> values) {
throw new UnsupportedOperationException("This map is immutable.");
}
@Override
public Map<K, V> 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<V> get(Object key) {
return this.targetMap.get(key);
}
@Override
public List<V> put(K key, List<V> value) {
throw new UnsupportedOperationException("This map is immutable.");
}
@Override
public List<V> remove(Object key) {
throw new UnsupportedOperationException("This map is immutable.");
}
@Override
public void putAll(Map<? extends K, ? extends List<V>> m) {
this.targetMap.putAll(m);
}
@Override
public void clear() {
throw new UnsupportedOperationException("This map is immutable.");
}
@Override
public Set<K> keySet() {
return Collections.unmodifiableSet(this.targetMap.keySet());
}
@Override
public Collection<List<V>> values() {
return Collections.unmodifiableCollection(this.targetMap.values());
}
@Override
public Set<Entry<K, List<V>>> 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();
}
}

View File

@ -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<String, String>(result);
this.queryParams = CollectionUtils.unmodifiableMultiValueMap(result);
}
return this.queryParams;
}

View File

@ -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();
}
}

View File

@ -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.
* <p>
* 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<String> undeliveredMessages;
public SockJsMessageDeliveryException(String sessionId, List<String> undeliveredMessages, Throwable cause) {
super("Failed to deliver message(s) " + undeliveredMessages + " for session " + sessionId, sessionId, cause);
this.undeliveredMessages = undeliveredMessages;
}
public List<String> getUndeliveredMessages() {
return this.undeliveredMessages;
}
}

View File

@ -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.
* <p>
* 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 <async-support>true</async-support>} element to servlet and filter declarations
* in web.xml.
* <p>
* 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.
* <p>
* See the "Base URL", "Static URLs", and "Session URLs" sections of the <a
* href="http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html">SockJS
* protocol</a> 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;
}

View File

@ -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);
}
}

View File

@ -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).
* <p>
* 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/**").
* <p>
* 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 <a href="https://github.com/sockjs/sockjs-client">SockJS
* client</a> 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}
* <p>
* 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<HttpMethod> httpMethods) throws IOException {
protected void sendMethodNotAllowed(ServerHttpResponse response, List<HttpMethod> httpMethods) {
logger.debug("Sending Method Not Allowed (405)");
response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
response.getHeaders().setAllow(new HashSet<HttpMethod>(httpMethods));

View File

@ -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

View File

@ -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:
*
* <pre>
* a["message1","message2"]

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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") {

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<String> undelivered = new ArrayList<String>(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();

View File

@ -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();
}

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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);
}
}

View File

@ -151,7 +151,7 @@ public class AbstractHttpSockJsSessionTests extends BaseAbstractSockJsSessionTes
}
@Override
protected void flushCache() throws IOException {
protected void flushCache() {
this.cacheFlushed = true;
}

View File

@ -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<T
verifyNoMoreInteractions(this.webSocketHandler);
}
@Test
public void delegateMessagesWithErrorAndConnectionClosing() throws Exception {
WebSocketHandler wsHandler = new ExceptionWebSocketHandlerDecorator(this.webSocketHandler);
TestSockJsSession sockJsSession = new TestSockJsSession("1", this.sockJsConfig, wsHandler);
String msg1 = "message 1";
String msg2 = "message 2";
String msg3 = "message 3";
doThrow(new IOException()).when(this.webSocketHandler).handleMessage(sockJsSession, new TextMessage(msg2));
sockJsSession.delegateConnectionEstablished();
try {
sockJsSession.delegateMessages(new String[] { msg1, msg2, msg3 });
fail("expected exception");
}
catch (SockJsMessageDeliveryException ex) {
assertEquals(Arrays.asList(msg3), ex.getUndeliveredMessages());
verify(this.webSocketHandler).afterConnectionEstablished(sockJsSession);
verify(this.webSocketHandler).handleMessage(sockJsSession, new TextMessage(msg1));
verify(this.webSocketHandler).handleMessage(sockJsSession, new TextMessage(msg2));
verify(this.webSocketHandler).afterConnectionClosed(sockJsSession, CloseStatus.SERVER_ERROR);
verifyNoMoreInteractions(this.webSocketHandler);
}
}
@Test
public void delegateConnectionClosed() throws Exception {
this.session.delegateConnectionEstablished();
@ -112,17 +142,17 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests<T
assertNew();
this.session.close();
assertNull("Close not ignored for a new session", this.session.getStatus());
assertNull("Close not ignored for a new session", this.session.getCloseStatus());
this.session.delegateConnectionEstablished();
assertOpen();
this.session.close();
assertClosed();
assertEquals(3000, this.session.getStatus().getCode());
assertEquals(3000, this.session.getCloseStatus().getCode());
this.session.close(CloseStatus.SERVER_ERROR);
assertEquals("Close should be ignored if already closed", 3000, this.session.getStatus().getCode());
assertEquals("Close should be ignored if already closed", 3000, this.session.getCloseStatus().getCode());
}
@Test
@ -152,7 +182,7 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests<T
assertEquals(1, this.session.getNumberOfLastActiveTimeUpdates());
assertTrue(this.session.didCancelHeartbeat());
assertEquals(new CloseStatus(3000, "Go away!"), this.session.getStatus());
assertEquals(new CloseStatus(3000, "Go away!"), this.session.getCloseStatus());
assertClosed();
verify(this.webSocketHandler).afterConnectionClosed(this.session, new CloseStatus(3000, "Go away!"));
}
@ -160,13 +190,13 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests<T
@Test
public void closeWithWriteFrameExceptions() throws Exception {
this.session.setExceptionOnWriteFrame(new IOException());
this.session.setExceptionOnWrite(new IOException());
this.session.delegateConnectionEstablished();
this.session.setActive(true);
this.session.close();
assertEquals(new CloseStatus(3000, "Go away!"), this.session.getStatus());
assertEquals(new CloseStatus(3000, "Go away!"), this.session.getCloseStatus());
assertClosed();
}
@ -179,7 +209,7 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests<T
this.session.setActive(true);
this.session.close(CloseStatus.NORMAL);
assertEquals(CloseStatus.NORMAL, this.session.getStatus());
assertEquals(CloseStatus.NORMAL, this.session.getCloseStatus());
assertClosed();
}
@ -193,28 +223,14 @@ public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests<T
@Test
public void writeFrameIoException() throws Exception {
this.session.setExceptionOnWriteFrame(new IOException());
this.session.setExceptionOnWrite(new IOException());
this.session.delegateConnectionEstablished();
try {
this.session.writeFrame(SockJsFrame.openFrame());
fail("expected exception");
}
catch (IOException ex) {
assertEquals(CloseStatus.SERVER_ERROR, this.session.getStatus());
verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.SERVER_ERROR);
}
}
@Test
public void writeFrameThrowable() throws Exception {
this.session.setExceptionOnWriteFrame(new NullPointerException());
this.session.delegateConnectionEstablished();
try {
this.session.writeFrame(SockJsFrame.openFrame());
fail("expected exception");
}
catch (SockJsProcessingException ex) {
assertEquals(CloseStatus.SERVER_ERROR, this.session.getStatus());
catch (SockJsTransportFailureException ex) {
assertEquals(CloseStatus.SERVER_ERROR, this.session.getCloseStatus());
verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.SERVER_ERROR);
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.transport.session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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;
/**
* @author Rossen Stoyanchev
*/
public class TestHttpSockJsSession extends AbstractHttpSockJsSession {
private boolean active;
private final List<SockJsFrame> 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<SockJsFrame> 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 {
}
}

View File

@ -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<SockJsFrame> sockJsFramesWritten = new ArrayList<>();
private final List<SockJsFrame> 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<SockJsFrame> 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;
}
}