Add SockJS path detection

This commit is contained in:
Rossen Stoyanchev
2013-05-05 20:51:37 -04:00
parent 97d225ba75
commit 7845ebc428
25 changed files with 654 additions and 133 deletions

View File

@@ -75,17 +75,9 @@ public class StandardWebSocketClient implements WebSocketClient {
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException {
return doHandshake(webSocketHandler, httpHeaders, UriComponentsBuilder.fromUri(uri).build());
}
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler,
final HttpHeaders httpHeaders, UriComponents uriComponents) throws WebSocketConnectFailureException {
URI uri = uriComponents.toUri();
StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter();
session.setUri(uri);
session.setRemoteHostName(uriComponents.getHost());
session.setRemoteHostName(uri.getHost());
Endpoint endpoint = new StandardEndpointAdapter(webSocketHandler, session);
ClientEndpointConfig.Builder configBuidler = ClientEndpointConfig.Builder.create();

View File

@@ -133,19 +133,11 @@ public class JettyWebSocketClient implements WebSocketClient, SmartLifecycle {
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri)
throws WebSocketConnectFailureException {
return doHandshake(webSocketHandler, headers, UriComponentsBuilder.fromUri(uri).build());
}
public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, UriComponents uriComponents)
throws WebSocketConnectFailureException {
// TODO: populate headers
URI uri = uriComponents.toUri();
JettyWebSocketSessionAdapter session = new JettyWebSocketSessionAdapter();
session.setUri(uri);
session.setRemoteHostName(uriComponents.getHost());
session.setRemoteHostName(uri.getHost());
JettyWebSocketListenerAdapter listener = new JettyWebSocketListenerAdapter(webSocketHandler, session);

View File

@@ -149,7 +149,7 @@ public class DefaultHandshakeHandler implements HandshakeHandler {
protected void handleInvalidUpgradeHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
logger.debug("Invalid Upgrade header " + request.getHeaders().getUpgrade());
response.setStatusCode(HttpStatus.BAD_REQUEST);
response.getBody().write("Can \"Upgrade\" only to \"websocket\".".getBytes("UTF-8"));
response.getBody().write("Can \"Upgrade\" only to \"WebSocket\".".getBytes("UTF-8"));
}
protected void handleInvalidConnectHeader(ServerHttpRequest request, ServerHttpResponse response) throws IOException {
@@ -227,13 +227,13 @@ public class DefaultHandshakeHandler implements HandshakeHandler {
private RequestUpgradeStrategy create() {
String className;
if (tomcatWebSocketPresent) {
className = "org.springframework.websocket.server.support.TomcatRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.TomcatRequestUpgradeStrategy";
}
else if (glassFishWebSocketPresent) {
className = "org.springframework.websocket.server.support.GlassFishRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.GlassFishRequestUpgradeStrategy";
}
else if (jettyWebSocketPresent) {
className = "org.springframework.websocket.server.support.JettyRequestUpgradeStrategy";
className = "org.springframework.web.socket.server.support.JettyRequestUpgradeStrategy";
}
else {
throw new IllegalStateException("No suitable " + RequestUpgradeStrategy.class.getSimpleName());

View File

@@ -20,9 +20,7 @@
* {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for
* registering type-based endpoints,
* {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for
* instantiating annotated endpoints through Spring, and
* {@link org.springframework.websocket.server.support.EndpointHandshakeHandler}
* for integrating endpoints into HTTP request processing.
* instantiating annotated endpoints through Spring.
*/
package org.springframework.web.socket.server.endpoint;

View File

@@ -83,7 +83,6 @@ public abstract class AbstractServerSockJsSession extends AbstractSockJsSession
disconnect(status);
}
// TODO: close status/reason
protected abstract void disconnect(CloseStatus status) throws IOException;
/**
@@ -104,12 +103,14 @@ public abstract class AbstractServerSockJsSession extends AbstractSockJsSession
else {
logger.warn("Terminating connection due to failure to send message: " + ex.getMessage());
}
close();
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());
close();
disconnect(CloseStatus.SERVER_ERROR);
close(CloseStatus.SERVER_ERROR);
throw new SockJsRuntimeException("Failed to write " + frame, ex);
}
}

View File

@@ -17,11 +17,16 @@ package org.springframework.web.socket.sockjs;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -39,7 +44,18 @@ import org.springframework.util.StringUtils;
import org.springframework.web.socket.WebSocketHandler;
/**
* Provides support for SockJS configuration options and serves the static SockJS URLs.
* An abstract class for {@link SockJsService} implementations. Provides configuration
* support, SockJS path resolution, and processing for static SockJS requests (e.g.
* "/info", "/iframe.html", etc). Sub-classes are responsible for handling transport
* requests.
*
* <p>
* It is expected that this service is mapped correctly to one or more prefixes such as
* "/echo" including all sub-URLs (e.g. "/echo/**"). A SockJS service itself is generally
* unaware of request mapping details but nevertheless must be able to extract the SockJS
* path, which is the portion of the request path following the prefix. In most cases,
* this class can auto-detect the SockJS path but you can also explicitly configure the
* list of valid prefixes with {@link #setValidSockJsPrefixes(String...)}.
*
* @author Rossen Stoyanchev
* @since 4.0
@@ -51,7 +67,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
private static final int ONE_YEAR = 365 * 24 * 60 * 60;
private String name = "SockJS Service " + ObjectUtils.getIdentityHexString(this);
private String name = "SockJSService@" + ObjectUtils.getIdentityHexString(this);
private String clientLibraryUrl = "https://d1fxtkz8shb9d2.cloudfront.net/sockjs-0.3.4.min.js";
@@ -67,6 +83,9 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
private final TaskScheduler taskScheduler;
private final List<String> sockJsPrefixes = new ArrayList<String>();
private final Set<String> sockJsPathCache = new CopyOnWriteArraySet<String>();
public AbstractSockJsService(TaskScheduler scheduler) {
@@ -85,6 +104,38 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
return this.name;
}
/**
* Use this property to configure one or more prefixes that this SockJS service is
* allowed to serve. The prefix (e.g. "/echo") is needed to extract the SockJS
* specific portion of the URL (e.g. "${prefix}/info", "${prefix}/iframe.html", etc).
* <p>
* This property is not strictly required. In most cases, the SockJS path can be
* auto-detected since the initial request from the SockJS client is of the form
* "{prefix}/info". Assuming the SockJS service is mapped correctly (e.g. using
* Ant-style pattern "/echo/**") this should work fine. This property can be used
* to configure explicitly the prefixes this service is allowed to service.
*
* @param prefixes the prefixes to use; prefixes do not need to include the portions
* of the path that represent Servlet container context or Servlet path.
*/
public void setValidSockJsPrefixes(String... prefixes) {
this.sockJsPrefixes.clear();
for (String prefix : prefixes) {
if (prefix.endsWith("/") && (prefix.length() > 1)) {
prefix = prefix.substring(0, prefix.length() - 1);
}
this.sockJsPrefixes.add(prefix);
}
// sort with longest prefix at the top
Collections.sort(this.sockJsPrefixes, Collections.reverseOrder(new Comparator<String>() {
public int compare(String o1, String o2) {
return new Integer(o1.length()).compareTo(new Integer(o2.length()));
}
}));
}
/**
* Transports which don't support cross-domain communication natively (e.g.
* "eventsource", "htmlfile") rely on serving a simple page (using the
@@ -198,10 +249,18 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
*
* @throws Exception
*/
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
String sockJsPath, WebSocketHandler webSocketHandler) throws IOException, TransportErrorException {
public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
throws IOException, TransportErrorException {
logger.debug(request.getMethod() + " [" + sockJsPath + "]");
String sockJsPath = getSockJsPath(request);
if (sockJsPath == null) {
logger.warn("Could not determine SockJS path for URL \"" + request.getURI().getPath() +
". Consider setting validSockJsPrefixes.");
response.setStatusCode(HttpStatus.NOT_FOUND);
return;
}
logger.debug(request.getMethod() + " with SockJS path [" + sockJsPath + "]");
try {
request.getHeaders();
@@ -225,13 +284,13 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
return;
}
else if (sockJsPath.equals("/websocket")) {
handleRawWebSocketRequest(request, response, webSocketHandler);
handleRawWebSocketRequest(request, response, handler);
return;
}
String[] pathSegments = StringUtils.tokenizeToStringArray(sockJsPath.substring(1), "/");
if (pathSegments.length != 3) {
logger.debug("Expected /{server}/{session}/{transport} but got " + sockJsPath);
logger.warn("Expected \"/{server}/{session}/{transport}\" but got \"" + sockJsPath + "\"");
response.setStatusCode(HttpStatus.NOT_FOUND);
return;
}
@@ -245,13 +304,62 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
return;
}
handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), webSocketHandler);
handleTransportRequest(request, response, sessionId, TransportType.fromValue(transport), handler);
}
finally {
response.flush();
}
}
/**
* Return the SockJS path or null if the path could not be determined.
*/
private String getSockJsPath(ServerHttpRequest request) {
String path = request.getURI().getPath();
// SockJS prefix hints?
if (!this.sockJsPrefixes.isEmpty()) {
for (String prefix : this.sockJsPrefixes) {
int index = path.indexOf(prefix);
if (index != -1) {
this.sockJsPathCache.add(path.substring(0, index + prefix.length()));
return path.substring(index + prefix.length());
}
}
}
// SockJS info request?
if (path.endsWith("/info")) {
this.sockJsPathCache.add(path.substring(0, path.length() - 6));
return "/info";
}
// Have we seen this prefix before (following the initial /info request)?
String match = null;
for (String sockJsPath : this.sockJsPathCache) {
if (path.startsWith(sockJsPath)) {
if ((match == null) || (match.length() < sockJsPath.length())) {
match = sockJsPath;
}
}
}
if (match != null) {
return path.substring(match.length());
}
// SockJS greeting?
String pathNoSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
String lastSegment = pathNoSlash.substring(pathNoSlash.lastIndexOf('/') + 1);
if ((TransportType.fromValue(lastSegment) == null) && !lastSegment.startsWith("iframe")) {
this.sockJsPathCache.add(path);
return "";
}
return null;
}
protected abstract void handleRawWebSocketRequest(ServerHttpRequest request,
ServerHttpResponse response, WebSocketHandler webSocketHandler) throws IOException;
@@ -263,18 +371,18 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
protected boolean validateRequest(String serverId, String sessionId, String transport) {
if (!StringUtils.hasText(serverId) || !StringUtils.hasText(sessionId) || !StringUtils.hasText(transport)) {
logger.debug("Empty server, session, or transport value");
logger.warn("Empty server, session, or transport value");
return false;
}
// Server and session id's must not contain "."
if (serverId.contains(".") || sessionId.contains(".")) {
logger.debug("Server or session contain a \".\"");
logger.warn("Server or session contain a \".\"");
return false;
}
if (!isWebSocketEnabled() && transport.equals(TransportType.WEBSOCKET.value())) {
logger.debug("Websocket transport is disabled");
logger.warn("Websocket transport is disabled");
return false;
}
@@ -346,7 +454,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
response.setStatusCode(HttpStatus.NO_CONTENT);
addCorsHeaders(request, response, HttpMethod.GET, HttpMethod.OPTIONS);
addCorsHeaders(request, response, HttpMethod.OPTIONS, HttpMethod.GET);
addCacheHeaders(response);
}
else {
@@ -404,4 +512,5 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf
}
};
}

View File

@@ -217,7 +217,7 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
*/
public final void close() throws IOException {
close(CloseStatus.NORMAL);
close(new CloseStatus(3000, "Go away!"));
}
/**
@@ -225,7 +225,7 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess
* <p>Performs cleanup and notifies the {@link SockJsHandler}.
*/
public final void close(CloseStatus status) throws IOException {
if (!isClosed()) {
if (isOpen()) {
if (logger.isDebugEnabled()) {
logger.debug("Closing " + this + ", " + status);
}

View File

@@ -78,6 +78,30 @@ public class SockJsFrame {
return this.content.getBytes(Charset.forName("UTF-8"));
}
public static String escapeCharacters(char[] chars) {
StringBuilder result = new StringBuilder();
for (char ch : chars) {
if (isSockJsEscapeCharacter(ch)) {
result.append('\\').append('u');
String hex = Integer.toHexString(ch).toLowerCase();
for (int i = 0; i < (4 - hex.length()); i++) {
result.append('0');
}
result.append(hex);
}
else {
result.append(ch);
}
}
return result.toString();
}
private static boolean isSockJsEscapeCharacter(char ch) {
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F')
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F')
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF');
}
public String toString() {
String result = this.content;
if (result.length() > 80) {
@@ -101,7 +125,7 @@ public class SockJsFrame {
sb.append('"');
// TODO: dependency on Jackson
char[] quotedChars = JsonStringEncoder.getInstance().quoteAsString(messages[i]);
sb.append(escapeSockJsCharacters(quotedChars));
sb.append(escapeCharacters(quotedChars));
sb.append('"');
if (i < messages.length - 1) {
sb.append(',');
@@ -110,30 +134,6 @@ public class SockJsFrame {
sb.append(']');
return sb.toString();
}
private static String escapeSockJsCharacters(char[] chars) {
StringBuilder result = new StringBuilder();
for (char ch : chars) {
if (isSockJsEscapeCharacter(ch)) {
result.append('\\').append('u');
String hex = Integer.toHexString(ch).toLowerCase();
for (int i = 0; i < (4 - hex.length()); i++) {
result.append('0');
}
result.append(hex);
}
else {
result.append(ch);
}
}
return result.toString();
}
private static boolean isSockJsEscapeCharacter(char ch) {
return (ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u200C' && ch <= '\u200F')
|| (ch >= '\u2028' && ch <= '\u202F') || (ch >= '\u2060' && ch <= '\u206F')
|| (ch >= '\uFFF0' && ch <= '\uFFFF') || (ch >= '\uD800' && ch <= '\uDFFF');
}
}
public interface FrameFormat {

View File

@@ -23,13 +23,14 @@ import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
/**
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface SockJsService {
void handleRequest(ServerHttpRequest request, ServerHttpResponse response,
String sockJsPath, WebSocketHandler webSocketHandler) throws IOException, TransportErrorException;
void handleRequest(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler handler)
throws IOException, TransportErrorException;
}

View File

@@ -17,7 +17,9 @@
package org.springframework.web.socket.sockjs;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpMethod;
@@ -50,6 +52,14 @@ public enum TransportType {
private final List<String> headerHints;
private static final Map<String, TransportType> transportTypes = new HashMap<String, TransportType>();
static {
for (TransportType type : values()) {
transportTypes.put(type.value, type);
}
}
private TransportType(String value, HttpMethod httpMethod, String... headerHints) {
this.value = value;
@@ -57,6 +67,7 @@ public enum TransportType {
this.headerHints = Arrays.asList(headerHints);
}
public String value() {
return this.value;
}
@@ -80,13 +91,8 @@ public enum TransportType {
return this.headerHints.contains("jsessionid");
}
public static TransportType fromValue(String transportValue) {
for (TransportType type : values()) {
if (type.value().equals(transportValue)) {
return type;
}
}
throw new IllegalArgumentException("No matching constant for [" + transportValue + "]");
public static TransportType fromValue(String value) {
return transportTypes.get(value);
}
@Override

View File

@@ -177,7 +177,7 @@ public class DefaultSockJsService extends AbstractSockJsService {
if (!supportedMethod.equals(request.getMethod())) {
if (HttpMethod.OPTIONS.equals(request.getMethod()) && transportType.supportsCors()) {
response.setStatusCode(HttpStatus.NO_CONTENT);
addCorsHeaders(request, response, supportedMethod, HttpMethod.OPTIONS);
addCorsHeaders(request, response, HttpMethod.OPTIONS, supportedMethod);
addCacheHeaders(response);
}
else {

View File

@@ -32,8 +32,6 @@ import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator;
import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator;
import org.springframework.web.util.NestedServletException;
import org.springframework.web.util.UrlPathHelper;
/**
* @author Rossen Stoyanchev
@@ -41,29 +39,17 @@ import org.springframework.web.util.UrlPathHelper;
*/
public class SockJsHttpRequestHandler implements HttpRequestHandler {
private final String prefix;
private final SockJsService sockJsService;
private final WebSocketHandler webSocketHandler;
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
/**
* Class constructor with {@link SockJsHandler} instance ...
*
* @param prefix the path prefix for the SockJS service. All requests with a path
* that begins with the specified prefix will be handled by this service. In a
* Servlet container this is the path within the current servlet mapping.
*/
public SockJsHttpRequestHandler(String prefix, SockJsService sockJsService, WebSocketHandler webSocketHandler) {
Assert.hasText(prefix, "prefix is required");
public SockJsHttpRequestHandler(SockJsService sockJsService, WebSocketHandler webSocketHandler) {
Assert.notNull(sockJsService, "sockJsService is required");
Assert.notNull(webSocketHandler, "webSocketHandler is required");
this.prefix = prefix;
this.sockJsService = sockJsService;
this.webSocketHandler = decorateWebSocketHandler(webSocketHandler);
}
@@ -79,35 +65,14 @@ public class SockJsHttpRequestHandler implements HttpRequestHandler {
return new LoggingWebSocketHandlerDecorator(handler);
}
public String getPrefix() {
return this.prefix;
}
public String getPattern() {
return this.prefix + "/**";
}
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
Assert.isTrue(lookupPath.startsWith(this.prefix),
"Request path does not match the prefix of the SockJsService " + this.prefix);
String sockJsPath = lookupPath.substring(prefix.length());
ServerHttpRequest httpRequest = new AsyncServletServerHttpRequest(request, response);
ServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
try {
this.sockJsService.handleRequest(httpRequest, httpResponse, sockJsPath, this.webSocketHandler);
}
catch (Exception ex) {
// TODO
throw new NestedServletException("SockJS service failure", ex);
}
this.sockJsService.handleRequest(httpRequest, httpResponse, this.webSocketHandler);
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.sockjs.AbstractSockJsSession;
import org.springframework.web.socket.sockjs.SockJsFrame;
import org.springframework.web.socket.sockjs.SockJsRuntimeException;
import org.springframework.web.socket.sockjs.TransportErrorException;
import org.springframework.web.socket.sockjs.TransportHandler;
@@ -61,6 +62,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
if (session == null) {
response.setStatusCode(HttpStatus.NOT_FOUND);
logger.warn("Session not found");
return;
}
@@ -75,22 +77,28 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
messages = readMessages(request);
}
catch (JsonMappingException ex) {
logger.error("Failed to read message: ", ex);
sendInternalServerError(response, "Payload expected.", session.getId());
return;
}
catch (IOException ex) {
logger.error("Failed to read message: ", ex);
sendInternalServerError(response, "Broken JSON encoding.", session.getId());
return;
}
catch (Throwable t) {
logger.error("Failed to read message: ", t);
sendInternalServerError(response, "Failed to process messages", session.getId());
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Received messages: " + Arrays.asList(messages));
logger.trace("Received message(s): " + Arrays.asList(messages));
}
response.setStatusCode(getResponseStatus());
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8")));
try {
session.delegateMessages(messages);
}
@@ -98,9 +106,6 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport
ExceptionWebSocketHandlerDecorator.tryCloseWithError(session, t, logger);
throw new SockJsRuntimeException("Unhandled WebSocketHandler error in " + this, t);
}
response.setStatusCode(getResponseStatus());
response.getHeaders().setContentType(new MediaType("text", "plain", Charset.forName("UTF-8")));
}
protected void sendInternalServerError(ServerHttpResponse response, String error,

View File

@@ -157,12 +157,13 @@ public abstract class AbstractHttpServerSockJsSession extends AbstractServerSock
protected synchronized void resetRequest() {
updateLastActiveTime();
if (isActive()) {
if (isActive() && this.asyncRequest.isAsyncStarted()) {
try {
logger.debug("Completing async request");
this.asyncRequest.completeAsync();
}
catch (Throwable ex) {
logger.warn("Failed to complete async request: " + ex.getMessage());
logger.error("Failed to complete async request: " + ex.getMessage());
}
}
this.asyncRequest = null;

View File

@@ -41,7 +41,11 @@ public class StreamingServerSockJsSession extends AbstractHttpServerSockJsSessio
FrameFormat frameFormat) throws TransportErrorException {
super.setInitialRequest(request, response, frameFormat);
super.setLongPollingRequest(request, response, frameFormat);
// the WebSocketHandler delegate may have closed the session
if (!isClosed()) {
super.setLongPollingRequest(request, response, frameFormat);
}
}
protected void flushCache() throws IOException {

View File

@@ -62,7 +62,7 @@ public class WebSocketServerSockJsSession extends AbstractServerSockJsSession {
@Override
public boolean isActive() {
return this.webSocketSession.isOpen();
return ((this.webSocketSession != null) && this.webSocketSession.isOpen());
}
public void handleMessage(TextMessage message, WebSocketSession wsSession) throws Exception {