Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v2.4.0

### Feb 02, 2026
- Enhancement: Retry mechanism added

## v2.3.2

### Jan 05, 2026
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.contentstack.sdk</groupId>
<artifactId>java</artifactId>
<version>2.3.2</version>
<version>2.4.0</version>
<packaging>jar</packaging>
<name>contentstack-java</name>
<description>Java SDK for Contentstack Content Delivery API</description>
Expand DownExpand Up@@ -314,7 +314,7 @@
<configuration>
<!-- Tests are skipped by default; use -Dtest to specify which tests to run -->
<!-- Example: -Dtest='*IT' for integration tests, -Dtest='Test*' for unit tests -->
<skipTests>true</skipTests>
<!-- <skipTests>true</skipTests> -->
<!-- OPTIMIZED: Parallel execution with controlled concurrency -->
<parallel>classes</parallel>
<threadCount>4</threadCount>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/contentstack/sdk/Config.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@ public class Config {
protected Proxy proxy = null;
protected String[] earlyAccess = null;
protected ConnectionPool connectionPool = new ConnectionPool();
protected RetryOptions retryOptions = new RetryOptions();
public String releaseId;
public String previewTimestamp;

Expand DownExpand Up@@ -129,6 +130,26 @@ public void setPlugins(List<ContentstackPlugin> plugins) {
this.plugins = plugins;
}

/**
* Sets the retry options.
*
* @param retryOptions the retry options
* @return the config
*/
public Config setRetryOptions(RetryOptions retryOptions) {
this.retryOptions = retryOptions;
return this;
}

/**
* Gets the retry options.
*
* @return the retry options
*/
public RetryOptions getRetryOptions() {
return this.retryOptions;
}

/**
* Gets host.
*
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/contentstack/sdk/CustomBackoffStrategy.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
package com.contentstack.sdk;

import java.io.IOException;

@FunctionalInterface
public interface CustomBackoffStrategy {
/**
* Calculates delay before next retry.
*
* @param attempt current attempt number (0-based)
* @param statusCode HTTP status code (or -1 for network errors)
* @param exception the exception that occurred (may be null)
* @return delay in milliseconds before next retry
*/
long calculateDelay(int attempt, int statusCode, IOException exception);
}
137 changes: 137 additions & 0 deletions src/main/java/com/contentstack/sdk/RetryInterceptor.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
package com.contentstack.sdk;

import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Logger;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class RetryInterceptor implements Interceptor {

private static final Logger logger = Logger.getLogger(RetryInterceptor.class.getName());
private final RetryOptions retryOptions;


public RetryInterceptor(RetryOptions retryOptions) {
if (retryOptions == null) {
throw new NullPointerException("RetryOptions cannot be null");
}
this.retryOptions = retryOptions;
}

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException lastException = null;

// If retry is disabled, just proceed with the request once
if (!retryOptions.isRetryEnabled()) {
return chain.proceed(request);
}

int attempt = 0;
// retryLimit means number of retries, so total attempts = 1 initial + retryLimit retries
int maxAttempts = retryOptions.getRetryLimit() + 1;

while (attempt < maxAttempts) {

try {
if(response != null) {
response.close();
}
response = chain.proceed(request);

if (shouldRetry(response.code()) && (attempt + 1) < maxAttempts) {
logger.fine("Retry attempt " + (attempt + 1) + " for status " + response.code() + " on " + request.url());

long delay = calculateDelay(attempt, response.code(), null);
Thread.sleep(delay);
attempt++;
continue;
}
return response;

} catch (IOException e) {
// Network error occurred
lastException = e;

if ((attempt + 1) < maxAttempts) {
try {
long delay = calculateDelay(attempt, -1, e);
Thread.sleep(delay);
attempt++;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", ie);
}
continue;
} else {
// No more retries, throw the exception
throw e;
}

} catch (InterruptedException e) {
// Thread was interrupted during sleep
Thread.currentThread().interrupt();
if (response != null) response.close();
throw new IOException("Retry interrupted", e);
}
}

// Should not reach here normally
if (lastException != null) {
throw lastException;
}
return response;
}

/**
* Determines if a status code should trigger a retry.
*
* @param statusCode HTTP status code
* @return true if this status code is retryable
*/
private boolean shouldRetry(int statusCode) {
return Arrays.stream(retryOptions.getRetryableStatusCodes()).anyMatch(code -> code == statusCode);
}

/**
* Calculates the delay before the next retry attempt.
*
* @param attempt current attempt number (0-based)
* @param statusCode HTTP status code (-1 for network errors)
* @param exception the IOException that occurred (may be null)
* @return delay in milliseconds
*/
private long calculateDelay(int attempt, int statusCode, IOException exception) {

if(retryOptions.hasCustomBackoff()) {
return retryOptions.getCustomBackoffStrategy().calculateDelay(attempt, statusCode, exception);
}
long baseDelay = retryOptions.getRetryDelay();

switch (retryOptions.getBackoffStrategy()) {
case FIXED:
// baseDelay already set above
break;

case LINEAR:
baseDelay = retryOptions.getRetryDelay() * (attempt + 1);
break;

case EXPONENTIAL:
baseDelay = (long) (retryOptions.getRetryDelay() * Math.pow(2,attempt));
break;

default:
// baseDelay already set above
break;
}

return baseDelay;
}

}
Loading
Loading