diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29ca0887..b5553ee9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 2.4.5
+
+- [FIXED] Calling disconnect() while reconnecting now moves to DISCONNECTED immediately instead of waiting for the pong timeout (#376).
+- [FIXED] A ChannelAuthorizer that returns null or an empty string now causes an AuthorizationFailureException instead of a crash (#372).
+- [FIXED] Repeated reconnect attempts after user sign-in no longer crash with an IllegalStateException (#367).
+- [FIXED] The PusherEvent constructor now stores the user ID under user_id, so getUserId() returns it (#350).
+- [ADDED] PusherOptions.setPath for connecting to a custom URL path (#371).
+- [ADDED] setSslSocketFactory on HttpChannelAuthorizer and HttpUserAuthenticator, for custom TLS setups such as mutual TLS (#359).
+
## 2.4.4
- [CHANGED] pusher.pro narrower proguard rules.
diff --git a/README.md b/README.md
index d633f786..1807f066 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ The pusher-java-client is available in Maven Central.
com.pusher
pusher-java-client
- 2.4.4
+ 2.4.5
```
@@ -85,7 +85,7 @@ The pusher-java-client is available in Maven Central.
```groovy
dependencies {
- implementation 'com.pusher:pusher-java-client:2.4.4'
+ implementation 'com.pusher:pusher-java-client:2.4.5'
}
```
diff --git a/build.gradle b/build.gradle
index f35abd6b..ee9933a6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ plugins {
}
group = "com.pusher"
-version = "2.4.4"
+version = "2.4.5"
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
diff --git a/src/main/java/com/pusher/client/connection/websocket/WebSocketConnection.java b/src/main/java/com/pusher/client/connection/websocket/WebSocketConnection.java
index 4a397d79..81f650ce 100644
--- a/src/main/java/com/pusher/client/connection/websocket/WebSocketConnection.java
+++ b/src/main/java/com/pusher/client/connection/websocket/WebSocketConnection.java
@@ -45,6 +45,7 @@ public class WebSocketConnection implements InternalConnection, WebSocketListene
private final Consumer eventHandler;
private String socketId;
private int reconnectAttempts = 0;
+ private volatile Future> reconnectTimer;
public WebSocketConnection(
final String url,
@@ -93,7 +94,13 @@ private void tryConnecting() {
@Override
public void disconnect() {
factory.queueOnEventThread(() -> {
- if (canDisconnect()) {
+ if (state == ConnectionState.RECONNECTING) {
+ // The previous socket has already closed and won't call onClose,
+ // so finish disconnecting here instead of waiting for it.
+ underlyingConnection.removeWebSocketListener();
+ updateState(ConnectionState.DISCONNECTING);
+ cancelTimeoutsAndTransitionToDisconnected();
+ } else if (canDisconnect()) {
updateState(ConnectionState.DISCONNECTING);
underlyingConnection.close();
}
@@ -253,15 +260,16 @@ private void tryReconnecting() {
updateState(ConnectionState.RECONNECTING);
long reconnectInterval = Math.min(maxReconnectionGap, reconnectAttempts * reconnectAttempts);
- factory
+ reconnectTimer = factory
.getTimers()
.schedule(
- () -> {
+ // Check the state on the event thread so it can't race with disconnect()
+ () -> factory.queueOnEventThread(() -> {
if (state == ConnectionState.RECONNECTING) {
underlyingConnection.removeWebSocketListener();
tryConnecting();
}
- },
+ }),
reconnectInterval,
TimeUnit.SECONDS
);
@@ -275,6 +283,10 @@ private boolean shouldReconnect(int code) {
private void cancelTimeoutsAndTransitionToDisconnected() {
activityTimer.cancelTimeouts();
+ if (reconnectTimer != null) {
+ reconnectTimer.cancel(false);
+ reconnectTimer = null;
+ }
factory.queueOnEventThread(() -> {
if (state == ConnectionState.DISCONNECTING) {
diff --git a/src/test/java/com/pusher/client/connection/websocket/WebSocketConnectionTest.java b/src/test/java/com/pusher/client/connection/websocket/WebSocketConnectionTest.java
index 4a74abc7..296f6e85 100644
--- a/src/test/java/com/pusher/client/connection/websocket/WebSocketConnectionTest.java
+++ b/src/test/java/com/pusher/client/connection/websocket/WebSocketConnectionTest.java
@@ -6,6 +6,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -24,6 +25,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
@@ -34,6 +36,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -467,6 +470,57 @@ public Object answer(InvocationOnMock invocation) {
assertEquals(ConnectionState.DISCONNECTED, connection.getState());
}
+ @Test
+ public void testDisconnectInReconnectingStateTransitionsToDisconnected() {
+ connect();
+ connection.onClose(3999, "reason", true);
+ assertEquals(ConnectionState.RECONNECTING, connection.getState());
+
+ connection.disconnect();
+
+ assertEquals(ConnectionState.DISCONNECTED, connection.getState());
+ verify(mockEventListener)
+ .onConnectionStateChange(new ConnectionStateChange(ConnectionState.RECONNECTING, ConnectionState.DISCONNECTING));
+ verify(mockEventListener)
+ .onConnectionStateChange(new ConnectionStateChange(ConnectionState.DISCONNECTING, ConnectionState.DISCONNECTED));
+ verify(mockUnderlyingConnection, never()).close();
+ verify(factory).shutdownThreads();
+ }
+
+ @Test
+ public void testDisconnectInReconnectingStateCancelsPendingReconnect() {
+ final ScheduledFuture> reconnectFuture = mock(ScheduledFuture.class);
+ when(factory.getTimers()).thenReturn(scheduledExecutorService);
+ doReturn(reconnectFuture)
+ .when(scheduledExecutorService)
+ .schedule(any(Runnable.class), any(Long.class), any(TimeUnit.class));
+
+ connection.connect();
+ connection.onClose(500, "reason", true);
+ assertEquals(ConnectionState.RECONNECTING, connection.getState());
+
+ connection.disconnect();
+
+ verify(reconnectFuture).cancel(false);
+ }
+
+ @Test
+ public void testReconnectThatRunsAfterDisconnectDoesNotReconnect() throws SSLException {
+ final ArgumentCaptor reconnect = ArgumentCaptor.forClass(Runnable.class);
+ when(factory.getTimers()).thenReturn(scheduledExecutorService);
+
+ connection.connect();
+ connection.onClose(500, "reason", true);
+ verify(scheduledExecutorService).schedule(reconnect.capture(), any(Long.class), any(TimeUnit.class));
+
+ connection.disconnect();
+ // Simulate the reconnect task already running when disconnect() cancelled it
+ reconnect.getValue().run();
+
+ verify(factory, times(1)).newWebSocketClientWrapper(any(URI.class), any(Proxy.class), any(WebSocketConnection.class));
+ assertEquals(ConnectionState.DISCONNECTED, connection.getState());
+ }
+
/* end of tests */
private void connect() {