Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/[^/]+$");
}

}
4 changes: 4 additions & 0 deletions src/main/java/com/contentstack/cms/models/OAuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ public String getFormattedAuthorizationEndpoint() {
if (hostname.contains("contentstack")) {
hostname = hostname
.replaceAll("-api\\.", "-app.") // eu-api.contentstack.com -> eu-app.contentstack.com
.replaceAll("-api-", "-app-") // eu-api-contentstack.com -> eu-app-contentstack.com
.replaceAll("^api\\.", "app.") // api.contentstack.io -> app.contentstack.io
.replaceAll("^api-", "app-") // api-contentstack.com -> app-contentstack.com
.replaceAll("\\.io$", ".com"); // *.io -> *.com
} else {
hostname = Util.OAUTH_APP_HOST;
Expand All @@ -107,7 +109,9 @@ public String getTokenEndpoint() {
if (hostname.contains("contentstack")) {
hostname = hostname
.replaceAll("-api\\.", "-developerhub-api.") // eu-api.contentstack.com -> eu-developerhub-api.contentstack.com
.replaceAll("-api-", "-developerhub-api-") // eu-api-contentstack.com -> eu-developerhub-api-contentstack.com
.replaceAll("^api\\.", "developerhub-api.") // api.contentstack.io -> developerhub-api.contentstack.io
.replaceAll("^api-", "developerhub-api-") // api-contentstack.com -> developerhub-api-contentstack.com
.replaceAll("\\.io$", ".com"); // *.io -> *.com
} else {
hostname = Util.OAUTH_API_HOST;
Expand Down
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