Skip to content
Draft
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
9 changes: 2 additions & 7 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
# Changelog

## v1.10.2

### Jan 12, 2026

- Improved error messages

## v1.10.1

### Jan 05, 2026
### Jan 12, 2026

- Snyk Fixes
- Improved error messages

## v1.10.0

Expand Down
7 changes: 6 additions & 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.10.2</version>
<version>1.10.1</version>
<description>Contentstack Java Management SDK for Content Management API, Contentstack is a headless CMS with an
API-first approach
</description>
Expand Down Expand Up @@ -245,6 +245,11 @@
<version>3.0.0-M5</version>
<configuration>
<includes>
<!-- Run all test files following Maven naming conventions -->
<include>**/Test*.java</include>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
<include>**/*TestCase.java</include>
<include>**/*TestSuite.java</include>
</includes>
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/contentstack/cms/Contentstack.java
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,12 @@ private OkHttpClient httpClient(Contentstack contentstack, Boolean retryOnFailur
builder.addInterceptor(this.oauthInterceptor);
} else {
this.authInterceptor = contentstack.interceptor = new AuthInterceptor();

// Configure early access if needed
if (this.earlyAccess != null) {
this.authInterceptor.setEarlyAccess(this.earlyAccess);
}

builder.addInterceptor(this.authInterceptor);
}

Expand Down
37 changes: 32 additions & 5 deletions src/main/java/com/contentstack/cms/core/AuthInterceptor.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.contentstack.cms.core;

import java.io.IOException;

import org.jetbrains.annotations.NotNull;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;

/**
* <b>The type Header interceptor that extends Interceptor</b>
Expand Down Expand Up @@ -73,16 +74,42 @@ public void setEarlyAccess(String[] earlyAccess) {
@Override
public Response intercept(Chain chain) throws IOException {
final String xUserAgent = Util.SDK_NAME + "/v" + Util.SDK_VERSION;
Request.Builder request = chain.request().newBuilder().header(Util.X_USER_AGENT, xUserAgent).header(Util.USER_AGENT, Util.defaultUserAgent()).header(Util.CONTENT_TYPE, Util.CONTENT_TYPE_VALUE);
Request originalRequest = chain.request();
Request.Builder request = originalRequest.newBuilder()
.header(Util.X_USER_AGENT, xUserAgent)
.header(Util.USER_AGENT, Util.defaultUserAgent());

// Skip Content-Type header for DELETE /releases/{release_uid} request
// to avoid "Body cannot be empty when content-type is set to 'application/json'" error
if (!isDeleteReleaseRequest(originalRequest)) {
request.header(Util.CONTENT_TYPE, Util.CONTENT_TYPE_VALUE);
}

if (this.authtoken != null) {
request.addHeader(Util.AUTHTOKEN, this.authtoken);
}
if (this.earlyAccess!=null && this.earlyAccess.length > 0) {

if (this.earlyAccess != null && this.earlyAccess.length > 0) {
String commaSeparated = String.join(", ", earlyAccess);
request.addHeader(Util.EARLY_ACCESS_HEADER, commaSeparated);
}
return chain.proceed(request.build());
}

/**
* Checks if the request is a DELETE request to /releases/{release_uid} endpoint.
* This endpoint should not have Content-Type header as it doesn't accept a body.
*
* @param request The HTTP request to check
* @return true if this is a DELETE /releases/{release_uid} request
*/
private boolean isDeleteReleaseRequest(Request request) {
if (!"DELETE".equals(request.method())) {
return false;
}
String path = request.url().encodedPath();
// Match pattern: /v3/releases/{release_uid} (no trailing path segments)
return path.matches(".*/releases/[^/]+$");
}

}
28 changes: 26 additions & 2 deletions src/main/java/com/contentstack/cms/oauth/OAuthInterceptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,44 @@ public Response intercept(Chain chain) throws IOException {
Request.Builder requestBuilder = originalRequest.newBuilder()
.header("X-User-Agent", Util.defaultUserAgent())
.header("User-Agent", Util.defaultUserAgent())
.header("Content-Type", originalRequest.url().toString().contains("/token") ? "application/x-www-form-urlencoded" : "application/json")
.header("x-header-ea", earlyAccess != null ? String.join(",", earlyAccess) : "true");

// Skip Content-Type header for DELETE /releases/{release_uid} request
// to avoid "Body cannot be empty when content-type is set to 'application/json'" error
if (!isDeleteReleaseRequest(originalRequest)) {
String contentType = originalRequest.url().toString().contains("/token")
? "application/x-www-form-urlencoded"
: "application/json";
requestBuilder.header("Content-Type", contentType);
}

// Skip auth header for token endpoints
if (!originalRequest.url().toString().contains("/token")) {
if (oauthHandler.getTokens() != null && oauthHandler.getTokens().hasAccessToken()) {
requestBuilder.header("Authorization", "Bearer " + oauthHandler.getAccessToken());

}
}

// Execute request with retry and refresh handling
return executeRequest(chain, requestBuilder.build(), 0);
}

/**
* Checks if the request is a DELETE request to /releases/{release_uid} endpoint.
* This endpoint should not have Content-Type header as it doesn't accept a body.
*
* @param request The HTTP request to check
* @return true if this is a DELETE /releases/{release_uid} request
*/
private boolean isDeleteReleaseRequest(Request request) {
if (!"DELETE".equals(request.method())) {
return false;
}
String path = request.url().encodedPath();
// Match pattern: /v3/releases/{release_uid} (no trailing path segments)
return path.matches(".*/releases/[^/]+$");
}

private Response executeRequest(Chain chain, Request request, int retryCount) throws IOException {
// Skip token refresh for token endpoints to avoid infinite loops
if (request.url().toString().contains("/token")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ void testSetOrganizations() {
client.organization();
} catch (Exception e) {
System.out.println(e.getLocalizedMessage());
Assertions.assertEquals("Please Login to access user instance", e.getLocalizedMessage());
Assertions.assertEquals("Login or configure OAuth to continue. organization", e.getLocalizedMessage());
}
}

Expand All @@ -203,7 +203,7 @@ void testSetAuthtokenLogin() {
try {
client.login("fake@email.com", "fake@password");
} catch (Exception e) {
Assertions.assertEquals("User is already loggedIn, Please logout then try to login again", e.getMessage());
Assertions.assertEquals("Operation not allowed. You are already logged in.", e.getMessage());
}
Assertions.assertEquals("fake@authtoken", client.authtoken);
}
Expand All @@ -216,7 +216,7 @@ void testSetAuthtokenLoginWithTfa() {
params.put("tfaToken", "fake@tfa");
client.login("fake@email.com", "fake@password", params);
} catch (Exception e) {
Assertions.assertEquals("User is already loggedIn, Please logout then try to login again", e.getMessage());
Assertions.assertEquals("Operation not allowed. You are already logged in.", e.getMessage());
}
Assertions.assertEquals("fake@authtoken", client.authtoken);
}
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/com/contentstack/cms/UnitTestSuite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.contentstack.cms;

import com.contentstack.cms.core.AuthInterceptorTest;
import com.contentstack.cms.stack.EnvironmentUnitTest;
import com.contentstack.cms.stack.GlobalFieldUnitTests;
import com.contentstack.cms.stack.LocaleUnitTest;
import com.contentstack.cms.stack.ReleaseUnitTest;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.runner.RunWith;

/**
* Unit Test Suite for running all unit tests
* These tests don't require API access or credentials
*
* Note: Only public test classes can be included here.
* Many unit test classes in the project are package-private and
* cannot be referenced in this suite.
*/
@SuppressWarnings("deprecation")
@RunWith(JUnitPlatform.class)
@SelectClasses({
// Core tests
AuthInterceptorTest.class,
ContentstackUnitTest.class,

// Stack module tests (only public classes)
EnvironmentUnitTest.class,
GlobalFieldUnitTests.class,
LocaleUnitTest.class,
ReleaseUnitTest.class
})
public class UnitTestSuite {
}

Loading
Loading