diff --git a/changelog.md b/changelog.md
index 80f1373b..b69d012c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,11 @@
# Changelog
+## v1.11.2
+
+### Jun 01, 2026
+
+- Fix: `SocketTimeoutException` now correctly triggers the retry mechanism in `AuthInterceptor` and `OAuthInterceptor`. Previously, network-level timeouts bypassed retry logic entirely, causing `.setRetry(true)` to have no effect on timeout errors.
+
## v1.11.1
### Apr 06, 2026
diff --git a/pom.xml b/pom.xml
index c227e9ba..1ff53c24 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
cms
jar
contentstack-management-java
- 1.11.1
+ 1.11.2
Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
diff --git a/src/main/java/com/contentstack/cms/core/AuthInterceptor.java b/src/main/java/com/contentstack/cms/core/AuthInterceptor.java
index 80ee0f97..873f5912 100644
--- a/src/main/java/com/contentstack/cms/core/AuthInterceptor.java
+++ b/src/main/java/com/contentstack/cms/core/AuthInterceptor.java
@@ -1,6 +1,7 @@
package com.contentstack.cms.core;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import org.jetbrains.annotations.NotNull;
@@ -116,21 +117,35 @@ public void setRetryConfig(RetryConfig retryConfig) {
this.retryConfig = retryConfig != null ? retryConfig : RetryConfig.defaultConfig();
}
- private Response executeRequest(Chain chain, Request request, int retryCount) throws IOException{
- Response response = chain.proceed(request);
- int code = response.code();
- if(retryCount < retryConfig.getRetryLimit() && retryConfig.getRetryCondition().shouldRetry(code, null)){
- response.close();
- long delay = RetryUtil.calculateDelay(retryConfig, retryCount+1, code);
- try {
- Thread.sleep(delay);
- } catch (InterruptedException ex) {
- Thread.currentThread().interrupt();
- throw new IOException("Retry interrupted", ex);
+ private Response executeRequest(Chain chain, Request request, int retryCount) throws IOException {
+ try {
+ Response response = chain.proceed(request);
+ int code = response.code();
+ if (retryCount < retryConfig.getRetryLimit() && retryConfig.getRetryCondition().shouldRetry(code, null)) {
+ response.close();
+ long delay = RetryUtil.calculateDelay(retryConfig, retryCount + 1, code);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Retry interrupted", ex);
+ }
+ return executeRequest(chain, request, retryCount + 1);
}
- return executeRequest(chain, request, retryCount + 1);
+ return response;
+ } catch (SocketTimeoutException e) {
+ if (retryCount < retryConfig.getRetryLimit() && retryConfig.getRetryCondition().shouldRetry(0, e)) {
+ long delay = RetryUtil.calculateDelay(retryConfig, retryCount + 1, 0);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Retry interrupted", ex);
+ }
+ return executeRequest(chain, request, retryCount + 1);
+ }
+ throw e;
}
- return response;
}
}
diff --git a/src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java b/src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java
index 56f110a9..8feb5c63 100644
--- a/src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java
+++ b/src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java
@@ -1,6 +1,7 @@
package com.contentstack.cms.oauth;
import java.io.IOException;
+import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -112,7 +113,22 @@ private Response executeRequest(Chain chain, Request request, int retryCount) th
}
// Execute request
- Response response = chain.proceed(request);
+ Response response;
+ try {
+ response = chain.proceed(request);
+ } catch (SocketTimeoutException e) {
+ if (retryCount < retryConfig.getRetryLimit() && retryConfig.getRetryCondition().shouldRetry(0, e)) {
+ long delay = RetryUtil.calculateDelay(retryConfig, retryCount + 1, 0);
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new IOException("Retry interrupted", ex);
+ }
+ return executeRequest(chain, request, retryCount + 1);
+ }
+ throw e;
+ }
// Handle error responses
if (!response.isSuccessful() && retryCount < retryConfig.getRetryLimit()) {
diff --git a/src/test/java/com/contentstack/cms/core/AuthInterceptorTest.java b/src/test/java/com/contentstack/cms/core/AuthInterceptorTest.java
index 310f964d..efe1b867 100644
--- a/src/test/java/com/contentstack/cms/core/AuthInterceptorTest.java
+++ b/src/test/java/com/contentstack/cms/core/AuthInterceptorTest.java
@@ -7,6 +7,7 @@
import org.junit.jupiter.api.Test;
import java.io.IOException;
+import java.net.SocketTimeoutException;
public class AuthInterceptorTest {
@@ -166,6 +167,89 @@ public Call call() {
}
}
+ @Test
+ @Tag("unit")
+ public void testRetry_onSocketTimeout_thenSuccess_retriesAndReturnsSuccess() throws IOException {
+ authInterceptor.setRetryConfig(RetryConfig.builder().retryLimit(3).retryDelay(10).build());
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/user")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 1, 200);
+ try (Response response = authInterceptor.intercept(chain)) {
+ Assertions.assertEquals(200, response.code());
+ Assertions.assertEquals(2, chain.getProceedCount());
+ }
+ }
+
+ @Test
+ @Tag("unit")
+ public void testRetry_onSocketTimeout_exhaustsRetries_throws() {
+ authInterceptor.setRetryConfig(RetryConfig.builder().retryLimit(2).retryDelay(10).build());
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/user")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 5, 200);
+ Assertions.assertThrows(SocketTimeoutException.class, () -> authInterceptor.intercept(chain));
+ Assertions.assertEquals(3, chain.getProceedCount());
+ }
+
+ @Test
+ @Tag("unit")
+ public void testRetry_onSocketTimeout_zeroRetryLimit_throwsImmediately() {
+ authInterceptor.setRetryConfig(RetryConfig.builder().retryLimit(0).retryDelay(10).build());
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/user")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 5, 200);
+ Assertions.assertThrows(SocketTimeoutException.class, () -> authInterceptor.intercept(chain));
+ Assertions.assertEquals(1, chain.getProceedCount());
+ }
+
+ private static class TimeoutTestChain implements Interceptor.Chain {
+ private final Request originalRequest;
+ private final int timeoutCount;
+ private final int successCode;
+ private int proceedCount = 0;
+
+ TimeoutTestChain(Request request, int timeoutCount, int successCode) {
+ this.originalRequest = request;
+ this.timeoutCount = timeoutCount;
+ this.successCode = successCode;
+ }
+
+ int getProceedCount() { return proceedCount; }
+
+ @Override
+ public Request request() { return originalRequest; }
+
+ @Override
+ public Response proceed(Request request) throws IOException {
+ proceedCount++;
+ if (proceedCount <= timeoutCount) {
+ throw new SocketTimeoutException("timeout");
+ }
+ return new Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(successCode)
+ .message("OK")
+ .body(ResponseBody.create("{}", MediaType.parse("application/json")))
+ .build();
+ }
+
+ @Override public Connection connection() { return null; }
+ @Override public int connectTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withConnectTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public int readTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withReadTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public int writeTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withWriteTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public Call call() { return null; }
+ }
+
@Test
public void AuthInterceptor() {
AuthInterceptor expected = new AuthInterceptor("abc");
diff --git a/src/test/java/com/contentstack/cms/oauth/OAuthInterceptorTest.java b/src/test/java/com/contentstack/cms/oauth/OAuthInterceptorTest.java
new file mode 100644
index 00000000..580f9268
--- /dev/null
+++ b/src/test/java/com/contentstack/cms/oauth/OAuthInterceptorTest.java
@@ -0,0 +1,118 @@
+package com.contentstack.cms.oauth;
+
+import com.contentstack.cms.core.RetryConfig;
+import com.contentstack.cms.models.OAuthTokens;
+import okhttp3.*;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+@RunWith(MockitoJUnitRunner.class)
+public class OAuthInterceptorTest {
+
+ private OAuthInterceptor interceptor;
+
+ @Mock
+ private OAuthHandler mockHandler;
+
+ @Mock
+ private OAuthTokens mockTokens;
+
+ @Before
+ public void setup() {
+ Mockito.lenient().when(mockTokens.isExpired()).thenReturn(false);
+ Mockito.lenient().when(mockTokens.hasAccessToken()).thenReturn(true);
+ Mockito.lenient().when(mockHandler.getTokens()).thenReturn(mockTokens);
+ Mockito.lenient().when(mockHandler.getAccessToken()).thenReturn("test-access-token");
+
+ interceptor = new OAuthInterceptor(mockHandler);
+ interceptor.setRetryConfig(RetryConfig.builder().retryLimit(3).retryDelay(10).build());
+ }
+
+ @Test
+ public void testRetry_onSocketTimeout_thenSuccess_retriesAndReturnsSuccess() throws IOException {
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/content_types")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 1, 200);
+ try (Response response = interceptor.intercept(chain)) {
+ assertEquals(200, response.code());
+ assertEquals(2, chain.getProceedCount());
+ }
+ }
+
+ @Test
+ public void testRetry_onSocketTimeout_exhaustsRetries_throws() {
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/content_types")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 5, 200);
+ assertThrows(SocketTimeoutException.class, () -> interceptor.intercept(chain));
+ assertEquals(4, chain.getProceedCount()); // 1 initial + 3 retries
+ }
+
+ @Test
+ public void testRetry_onSocketTimeout_zeroRetryLimit_throwsImmediately() {
+ interceptor.setRetryConfig(RetryConfig.builder().retryLimit(0).retryDelay(10).build());
+ Request request = new Request.Builder()
+ .url("https://api.contentstack.io/v3/content_types")
+ .get()
+ .build();
+ TimeoutTestChain chain = new TimeoutTestChain(request, 5, 200);
+ assertThrows(SocketTimeoutException.class, () -> interceptor.intercept(chain));
+ assertEquals(1, chain.getProceedCount());
+ }
+
+ private static class TimeoutTestChain implements Interceptor.Chain {
+ private final Request originalRequest;
+ private final int timeoutCount;
+ private final int successCode;
+ private int proceedCount = 0;
+
+ TimeoutTestChain(Request request, int timeoutCount, int successCode) {
+ this.originalRequest = request;
+ this.timeoutCount = timeoutCount;
+ this.successCode = successCode;
+ }
+
+ int getProceedCount() { return proceedCount; }
+
+ @Override
+ public Request request() { return originalRequest; }
+
+ @Override
+ public Response proceed(Request request) throws IOException {
+ proceedCount++;
+ if (proceedCount <= timeoutCount) {
+ throw new SocketTimeoutException("timeout");
+ }
+ return new Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(successCode)
+ .message("OK")
+ .body(ResponseBody.create("{}", MediaType.parse("application/json")))
+ .build();
+ }
+
+ @Override public Connection connection() { return null; }
+ @Override public int connectTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withConnectTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public int readTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withReadTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public int writeTimeoutMillis() { return 0; }
+ @Override public Interceptor.Chain withWriteTimeout(int timeout, java.util.concurrent.TimeUnit unit) { return this; }
+ @Override public Call call() { return null; }
+ }
+}