Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# 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.
- Enhancement: Added `setProtocols(List<Protocol>)` to the Builder, allowing callers to restrict the HTTP protocol (e.g. force HTTP/1.1 via `Collections.singletonList(Protocol.HTTP_1_1)`) for environments where proxies or intermediaries have issues with HTTP/2.

## v1.11.1

### Apr 06, 2026
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>cms</artifactId>
<packaging>jar</packaging>
<name>contentstack-management-java</name>
<version>1.11.1</version>
<version>1.11.2</version>
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
</description>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/contentstack/cms/Contentstack.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import java.io.IOException;
import java.net.Proxy;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
Expand All @@ -30,6 +32,7 @@
import com.contentstack.cms.core.RetryConfig;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Response;
Expand Down Expand Up @@ -609,6 +612,7 @@ public static class Builder {
* evicted after 5 minutes of inactivity.
*/
private ConnectionPool connectionPool = new ConnectionPool(); // Connection
private List<Protocol> protocols = null;

/**
* Instantiates a new Builder.
Expand Down Expand Up @@ -713,6 +717,11 @@ public Builder setConnectTimeout(int connectTimeoutSeconds) {
return this;
}

public Builder setProtocols(@NotNull List<Protocol> protocols) {
this.protocols = protocols;
return this;
}

private static void validateTimeoutSeconds(int seconds, String name) {
if (seconds <= 0) {
throw new IllegalArgumentException(name + " must be positive.");
Expand Down Expand Up @@ -881,6 +890,9 @@ private OkHttpClient httpClient(Contentstack contentstack, Boolean retryOnFailur
.readTimeout(Duration.ofSeconds(readSec))
.writeTimeout(Duration.ofSeconds(writeSec))
.retryOnConnectionFailure(retryOnFailure);
if (this.protocols != null && !this.protocols.isEmpty()) {
builder.protocols(this.protocols);
}

// Add either OAuth or traditional auth interceptor
if (this.oauthConfig != null) {
Expand Down
41 changes: 28 additions & 13 deletions src/main/java/com/contentstack/cms/core/AuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.contentstack.cms.core;

import java.io.IOException;
import java.net.SocketTimeoutException;

import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -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;
}

}
18 changes: 17 additions & 1 deletion src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()) {
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/com/contentstack/cms/ContentstackUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.contentstack.cms.stack.Stack;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.ResponseBody;
import okhttp3.mockwebserver.MockResponse;
Expand All @@ -17,7 +18,9 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

Expand Down Expand Up @@ -160,6 +163,24 @@ void setVersion() {
Assertions.assertEquals("v8", contentstack.version);
}

@Test
void setProtocols_http1Only_buildsSuccessfully() {
List<Protocol> protocols = Collections.singletonList(Protocol.HTTP_1_1);
Contentstack contentstack = new Contentstack.Builder()
.setProtocols(protocols)
.build();
Assertions.assertNotNull(contentstack);
}

@Test
void setProtocols_http2AndHttp1_buildsSuccessfully() {
List<Protocol> protocols = List.of(Protocol.HTTP_2, Protocol.HTTP_1_1);
Contentstack contentstack = new Contentstack.Builder()
.setProtocols(protocols)
.build();
Assertions.assertNotNull(contentstack);
}

@Test
void setTimeout() {
Contentstack contentstack = new Contentstack.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.net.SocketTimeoutException;

public class AuthInterceptorTest {

Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading