diff --git a/.gitignore b/.gitignore
index 7e8a92bc..d1bfb78a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,4 +47,6 @@ src/main/res/
contentstack/src/androidTest/java/com/contentstack/sdk/SyncTestCase.java
# key file
-key.keystore
\ No newline at end of file
+key.keystore
+
+contentstack/src/main/resources/assets/regions.json
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e2a7f703..93222deb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# CHANGELOG
+## Version 4.3.0
+
+### Date: 29-Jun-2026
+
+### Enhancement
+
+- Feature: Dynamic endpoint resolution via `Endpoint.getContentstackEndpoint()` and `Builder.setRegion()` backed by the Contentstack Regions Registry.
+
## Version 4.2.2
### Date: 01-Jun-2026
diff --git a/contentstack/build.gradle b/contentstack/build.gradle
index 798afd6e..7f9d7353 100755
--- a/contentstack/build.gradle
+++ b/contentstack/build.gradle
@@ -7,7 +7,7 @@ plugins {
ext {
PUBLISH_GROUP_ID = 'com.contentstack.sdk'
PUBLISH_ARTIFACT_ID = 'android'
- PUBLISH_VERSION = '4.2.2'
+ PUBLISH_VERSION = '4.3.0'
}
android {
@@ -410,4 +410,16 @@ gradle.projectsEvaluated {
ut.enabled = false
}
}
-}
\ No newline at end of file
+}
+// Refresh the bundled regions.json from the Contentstack artifact registry.
+// Run whenever Contentstack adds new regions or service keys, then commit the
+// updated contentstack/src/main/resources/assets/regions.json.
+//
+// Usage:
+// ./gradlew :contentstack:refreshRegions
+tasks.register('refreshRegions', Exec) {
+ description = 'Download the latest regions.json from the Contentstack artifact registry.'
+ group = 'contentstack'
+ workingDir = rootProject.projectDir
+ commandLine 'bash', "${rootProject.projectDir}/scripts/download-regions.sh"
+}
diff --git a/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java
index 1f807b6e..3332df7c 100644
--- a/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java
+++ b/contentstack/src/androidTest/java/com/contentstack/sdk/AssetTestCase.java
@@ -162,9 +162,8 @@ public void test_AZURE_NA() throws Exception {
String DEFAULT_API_KEY = BuildConfig.APIKey;
String DEFAULT_DELIVERY_TOKEN = BuildConfig.deliveryToken;
String DEFAULT_ENV = BuildConfig.environment;
- String DEFAULT_HOST = BuildConfig.host;
- config.setHost(DEFAULT_HOST);
config.setRegion(Config.ContentstackRegion.AZURE_NA);
+ // Host is resolved from the region; no explicit setHost() so region resolution applies
Context appContext = InstrumentationRegistry.getTargetContext();
stack = Contentstack.stack(appContext, DEFAULT_API_KEY, DEFAULT_DELIVERY_TOKEN, DEFAULT_ENV, config);
assertEquals("azure-na-cdn.contentstack.com", config.getHost());
@@ -189,9 +188,8 @@ public void test_GCP_NA() throws Exception {
String DEFAULT_API_KEY = BuildConfig.APIKey;
String DEFAULT_DELIVERY_TOKEN = BuildConfig.deliveryToken;
String DEFAULT_ENV = BuildConfig.environment;
- String DEFAULT_HOST = BuildConfig.host;
- config.setHost(DEFAULT_HOST);
config.setRegion(Config.ContentstackRegion.GCP_NA);
+ // Host is resolved from the region; no explicit setHost() so region resolution applies
Context appContext = InstrumentationRegistry.getTargetContext();
stack = Contentstack.stack(appContext, DEFAULT_API_KEY, DEFAULT_DELIVERY_TOKEN, DEFAULT_ENV, config);
assertEquals("gcp-na-cdn.contentstack.com", config.getHost());
diff --git a/contentstack/src/main/java/com/contentstack/sdk/Config.java b/contentstack/src/main/java/com/contentstack/sdk/Config.java
index 0cd845ee..875862b8 100755
--- a/contentstack/src/main/java/com/contentstack/sdk/Config.java
+++ b/contentstack/src/main/java/com/contentstack/sdk/Config.java
@@ -17,6 +17,7 @@
public class Config {
protected String PROTOCOL = "https://";
protected String URL = "cdn.contentstack.io";
+ protected boolean hostOverridden = false;
protected String VERSION = "v3";
protected String environment = null;
protected String branch = null;
@@ -125,6 +126,7 @@ public Config() {
public void setHost(String hostName) {
if (!TextUtils.isEmpty(hostName)) {
URL = hostName;
+ hostOverridden = true;
}
}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java b/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java
index 5d10650e..ff859589 100755
--- a/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java
+++ b/contentstack/src/main/java/com/contentstack/sdk/Contentstack.java
@@ -8,6 +8,7 @@
import com.android.volley.toolbox.Volley;
import java.io.File;
+import java.util.Map;
import java.util.Objects;
/**
@@ -90,6 +91,60 @@ public static Stack stack(Context context, String apiKey, String deliveryToken,
}
+ /**
+ * Returns the Contentstack API URL for the given region and service.
+ *
+ *
Delegates to {@link Endpoint#getContentstackEndpoint(String, String)} — provided as a
+ * convenience so callers can reach endpoint resolution through the same top-level class they
+ * use to create stacks.
+ *
+ * @param region region ID or alias (e.g. {@code "na"}, {@code "eu"}, {@code "azure-na"})
+ * @param service service key (e.g. {@code "contentDelivery"}, {@code "contentManagement"})
+ * @return full URL including {@code https://} scheme
+ * @throws IllegalArgumentException if the region or service is not recognised
+ */
+ public static String getContentstackEndpoint(String region, String service) {
+ return Endpoint.getContentstackEndpoint(region, service);
+ }
+
+ /**
+ * Returns the Contentstack API URL for the given region and service, optionally stripping
+ * the {@code https://} scheme.
+ *
+ * @param region region ID or alias
+ * @param service service key
+ * @param omitHttps when {@code true}, returns the bare host without {@code https://}
+ * @return URL or bare host
+ * @throws IllegalArgumentException if the region or service is not recognised
+ */
+ public static String getContentstackEndpoint(String region, String service, boolean omitHttps) {
+ return Endpoint.getContentstackEndpoint(region, service, omitHttps);
+ }
+
+ /**
+ * Returns all service endpoints for the given region as an ordered map of service key to URL.
+ *
+ * @param region region ID or alias
+ * @return map of service key → full URL
+ * @throws IllegalArgumentException if the region is not recognised
+ */
+ public static Map getContentstackEndpoints(String region) {
+ return Endpoint.getAllEndpoints(region);
+ }
+
+ /**
+ * Returns all service endpoints for the given region, optionally stripping the
+ * {@code https://} scheme from every URL.
+ *
+ * @param region region ID or alias
+ * @param omitHttps when {@code true}, returns bare hosts without {@code https://}
+ * @return map of service key → URL or bare host
+ * @throws IllegalArgumentException if the region is not recognised
+ */
+ public static Map getContentstackEndpoints(String region, boolean omitHttps) {
+ return Endpoint.getAllEndpoints(region, omitHttps);
+ }
+
private static Stack initializeStack(Context appContext, String apiKey, String deliveryToken, Config config) {
Stack stack = new Stack(apiKey.trim());
stack.setHeader("api_key", apiKey);
diff --git a/contentstack/src/main/java/com/contentstack/sdk/Endpoint.java b/contentstack/src/main/java/com/contentstack/sdk/Endpoint.java
new file mode 100644
index 00000000..9d02c6e5
--- /dev/null
+++ b/contentstack/src/main/java/com/contentstack/sdk/Endpoint.java
@@ -0,0 +1,233 @@
+package com.contentstack.sdk;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.logging.Logger;
+
+/**
+ * Resolves Contentstack API endpoints for any region and service without hardcoding host strings.
+ *
+ * Resolution chain
+ *
+ * - In-memory cache — populated on the first call and reused for the process lifetime
+ * (zero I/O on every subsequent call).
+ * - Bundled {@code regions.json} — read from the classpath resource
+ * {@code /assets/regions.json} that is packaged inside the SDK. Works
+ * fully offline with zero latency.
+ * - Live download — if the requested region is not present in the bundled file
+ * (e.g. Contentstack added a new region after this SDK version was released), a single
+ * HTTP request is made to {@value #REGIONS_URL} to fetch the latest registry. The
+ * downloaded data replaces the in-memory cache so all subsequent lookups benefit from it.
+ * This attempt is made at most once per session to avoid repeated network
+ * calls for genuinely invalid region strings.
+ *
+ *
+ * Region matching is case-insensitive and treats {@code -} and {@code _} as equivalent
+ * separators, so {@code "AZURE_NA"}, {@code "azure-na"}, and {@code "Azure_NA"} all resolve
+ * to the same region.
+ *
+ *
Examples:
+ *
+ * String url = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
+ * // → "https://eu-cdn.contentstack.com"
+ *
+ * String host = Endpoint.getContentstackEndpoint("eu", "contentDelivery", true);
+ * // → "eu-cdn.contentstack.com"
+ *
+ * Map<String, String> all = Endpoint.getAllEndpoints("azure-na");
+ * // → {"contentDelivery": "https://azure-na-cdn.contentstack.com", ...}
+ *
+ */
+public class Endpoint {
+
+ static final String REGIONS_URL = "https://artifacts.contentstack.com/regions.json";
+
+ private static final Logger logger = Logger.getLogger(Endpoint.class.getSimpleName());
+
+ private static volatile JSONArray regionsCache = null;
+
+ private static volatile boolean liveRefreshDone = false;
+
+ private Endpoint() {
+ }
+
+ public static String getContentstackEndpoint(String region, String service) {
+ return getContentstackEndpoint(region, service, false);
+ }
+
+ public static String getContentstackEndpoint(String region, String service, boolean omitHttps) {
+ return getContentstackEndpoint(region, service, omitHttps, null);
+ }
+
+ /**
+ * Internal variant that routes the live-refresh fallback through the given {@code proxy}
+ * (typically the one configured on {@link Config}). Used by {@link Stack} so region
+ * resolution still works in proxy-only / VPN environments. A {@code null} proxy uses a
+ * direct connection.
+ */
+ static String getContentstackEndpoint(String region, String service, boolean omitHttps, Proxy proxy) {
+ if (region == null || region.trim().isEmpty()) {
+ throw new IllegalArgumentException("Empty region provided. Please provide a valid region.");
+ }
+ JSONObject regionRow = resolveRegion(region, proxy);
+ try {
+ JSONObject endpoints = regionRow.getJSONObject("endpoints");
+ if (!endpoints.has(service)) {
+ throw new IllegalArgumentException(
+ "Service \"" + service + "\" not found for region \"" + region + "\"");
+ }
+ String url = endpoints.getString(service);
+ return omitHttps ? stripHttps(url) : url;
+ } catch (JSONException e) {
+ throw new IllegalStateException("Malformed regions.json: " + e.getMessage(), e);
+ }
+ }
+
+ public static Map getAllEndpoints(String region) {
+ return getAllEndpoints(region, false);
+ }
+
+ public static Map getAllEndpoints(String region, boolean omitHttps) {
+ if (region == null || region.trim().isEmpty()) {
+ throw new IllegalArgumentException("Empty region provided. Please provide a valid region.");
+ }
+ JSONObject regionRow = resolveRegion(region, null);
+ try {
+ JSONObject endpoints = regionRow.getJSONObject("endpoints");
+ Map result = new LinkedHashMap<>();
+ Iterator keys = endpoints.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ String url = endpoints.getString(key);
+ result.put(key, omitHttps ? stripHttps(url) : url);
+ }
+ return result;
+ } catch (JSONException e) {
+ throw new IllegalStateException("Malformed regions.json: " + e.getMessage(), e);
+ }
+ }
+
+ static synchronized void resetCache() {
+ regionsCache = null;
+ liveRefreshDone = false;
+ }
+
+ private static JSONObject resolveRegion(String region, Proxy proxy) {
+ JSONArray regions = loadRegions(proxy);
+ try {
+ return findRegion(regions, region);
+ } catch (IllegalArgumentException notInBundled) {
+ if (!liveRefreshDone) {
+ JSONArray fresh = tryLiveRefresh(proxy);
+ if (fresh != null) {
+ try {
+ return findRegion(fresh, region);
+ } catch (IllegalArgumentException ignored) {
+ // fall through to re-throw the original error below
+ }
+ }
+ }
+ throw notInBundled;
+ }
+ }
+
+ private static synchronized JSONArray loadRegions(Proxy proxy) {
+ if (regionsCache != null) {
+ return regionsCache;
+ }
+ InputStream stream = Endpoint.class.getResourceAsStream("/assets/regions.json");
+ if (stream != null) {
+ try (Scanner scanner = new Scanner(stream, StandardCharsets.UTF_8.name())) {
+ String raw = scanner.useDelimiter("\\A").next();
+ JSONObject root = new JSONObject(raw);
+ regionsCache = root.getJSONArray("regions");
+ return regionsCache;
+ } catch (JSONException e) {
+ throw new IllegalStateException("Bundled regions.json is corrupt: " + e.getMessage(), e);
+ }
+ }
+ logger.warning("Bundled regions.json not found in classpath — attempting live download.");
+ JSONArray downloaded = tryLiveRefresh(proxy);
+ if (downloaded != null) {
+ return downloaded;
+ }
+ throw new IllegalStateException(
+ "regions.json not found in classpath and could not be downloaded from "
+ + REGIONS_URL + ". Ensure the SDK was built correctly, or check network access.");
+ }
+
+ private static synchronized JSONArray tryLiveRefresh(Proxy proxy) {
+ if (liveRefreshDone) {
+ return regionsCache;
+ }
+ liveRefreshDone = true;
+ try {
+ logger.info("Refreshing regions from " + REGIONS_URL);
+ URL url = new URL(REGIONS_URL);
+ HttpURLConnection conn = (HttpURLConnection) (proxy != null
+ ? url.openConnection(proxy)
+ : url.openConnection());
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(5_000);
+ conn.setReadTimeout(10_000);
+ conn.setRequestProperty("Accept", "application/json");
+ try (InputStream stream = conn.getInputStream();
+ Scanner scanner = new Scanner(stream, StandardCharsets.UTF_8.name())) {
+ String raw = scanner.useDelimiter("\\A").next();
+ JSONObject root = new JSONObject(raw);
+ regionsCache = root.getJSONArray("regions");
+ logger.info("regions.json refreshed from live URL (" + regionsCache.length() + " regions).");
+ return regionsCache;
+ }
+ } catch (Exception e) {
+ logger.warning("Live region refresh failed: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private static JSONObject findRegion(JSONArray regions, String region) {
+ String normalized = region.trim().toLowerCase().replace('_', '-');
+
+ try {
+ for (int i = 0; i < regions.length(); i++) {
+ JSONObject row = regions.getJSONObject(i);
+ if (row.getString("id").equals(normalized)) {
+ return row;
+ }
+ }
+
+ for (int i = 0; i < regions.length(); i++) {
+ JSONObject row = regions.getJSONObject(i);
+ JSONArray aliases = row.optJSONArray("alias");
+ if (aliases == null) {
+ continue;
+ }
+ for (int j = 0; j < aliases.length(); j++) {
+ String alias = aliases.getString(j).toLowerCase().replace('_', '-');
+ if (alias.equals(normalized)) {
+ return row;
+ }
+ }
+ }
+ } catch (JSONException e) {
+ throw new IllegalStateException("Malformed regions.json: " + e.getMessage(), e);
+ }
+
+ throw new IllegalArgumentException("Invalid region: " + region);
+ }
+
+ private static String stripHttps(String url) {
+ return url.replaceFirst("^https?://", "");
+ }
+}
diff --git a/contentstack/src/main/java/com/contentstack/sdk/Stack.java b/contentstack/src/main/java/com/contentstack/sdk/Stack.java
index 43aa73fb..1a7b1727 100755
--- a/contentstack/src/main/java/com/contentstack/sdk/Stack.java
+++ b/contentstack/src/main/java/com/contentstack/sdk/Stack.java
@@ -78,22 +78,19 @@ protected void setConfig(Config config) {
if (!TextUtils.isEmpty(config.environment)) {
setHeader("environment", config.environment);
}
- // Handle region setting first before any host overrides
- if (!config.region.name().isEmpty()) {
- String region = config.region.name().toLowerCase();
- if (!region.equalsIgnoreCase("us")) {
- if (region.equalsIgnoreCase("azure_na")) {
- config.setHost("azure-na-cdn.contentstack.com");
- } else if (region.equalsIgnoreCase("azure_eu")) {
- config.setHost("azure-eu-cdn.contentstack.com");
- } else if (region.equalsIgnoreCase("gcp_na")) {
- config.setHost("gcp-na-cdn.contentstack.com");
- } else if (region.equalsIgnoreCase("gcp_eu")) {
- config.setHost("gcp-eu-cdn.contentstack.com");
- } else if (region.equalsIgnoreCase("au")) {
- config.setHost("au-cdn.contentstack.com");
- } else {
- config.setHost(region + "-cdn.contentstack.io");
+ // Explicit host (set via Config.setHost()) always takes precedence over region resolution.
+ // When no host was explicitly set, resolve the content-delivery host from regions.json via
+ // Endpoint so that new regions are picked up without SDK changes.
+ if (!config.hostOverridden && !config.region.name().isEmpty()) {
+ String regionId = config.region.name().toLowerCase();
+ try {
+ // Route the live-refresh fallback through any configured proxy so region
+ // resolution still works in proxy-only / VPN environments.
+ config.URL = Endpoint.getContentstackEndpoint(regionId, "contentDelivery", true, config.getProxy());
+ } catch (IllegalArgumentException e) {
+ // Unrecognised region: apply the legacy prefix pattern for backward compatibility
+ if (!regionId.equalsIgnoreCase("us")) {
+ config.URL = regionId.replace("_", "-") + "-cdn.contentstack.com";
}
}
}
diff --git a/contentstack/src/test/java/com/contentstack/sdk/TestEndpoint.java b/contentstack/src/test/java/com/contentstack/sdk/TestEndpoint.java
new file mode 100644
index 00000000..9726c2b8
--- /dev/null
+++ b/contentstack/src/test/java/com/contentstack/sdk/TestEndpoint.java
@@ -0,0 +1,287 @@
+package com.contentstack.sdk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = 28, manifest = Config.NONE)
+public class TestEndpoint {
+
+ @After
+ public void resetCache() {
+ Endpoint.resetCache();
+ }
+
+ // ── canonical IDs ─────────────────────────────────────────────────────────
+
+ @Test
+ public void testNaContentDelivery() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentDelivery"));
+ }
+
+ @Test
+ public void testEuContentDelivery() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery"));
+ }
+
+ @Test
+ public void testAuContentDelivery() {
+ assertEquals("https://au-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("au", "contentDelivery"));
+ }
+
+ @Test
+ public void testAzureNaContentDelivery() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAzureEuContentDelivery() {
+ assertEquals("https://azure-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-eu", "contentDelivery"));
+ }
+
+ @Test
+ public void testGcpNaContentDelivery() {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testGcpEuContentDelivery() {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery"));
+ }
+
+ // ── aliases ───────────────────────────────────────────────────────────────
+
+ @Test
+ public void testAliasUsResolvesToNa() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("us", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasUppercaseEU() {
+ assertEquals("https://eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("EU", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAwsNaHyphen() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("aws-na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAwsNaUnderscore() {
+ assertEquals("https://cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("aws_na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAzureNaUnderscore() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure_na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasAzureNaUppercase() {
+ assertEquals("https://azure-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("AZURE_NA", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasGcpNaUnderscore() {
+ assertEquals("https://gcp-na-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("gcp_na", "contentDelivery"));
+ }
+
+ @Test
+ public void testAliasGcpEuUppercase() {
+ assertEquals("https://gcp-eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("GCP-EU", "contentDelivery"));
+ }
+
+ // ── services ──────────────────────────────────────────────────────────────
+
+ @Test
+ public void testNaContentManagement() {
+ assertEquals("https://api.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentManagement"));
+ }
+
+ @Test
+ public void testEuContentManagement() {
+ assertEquals("https://eu-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentManagement"));
+ }
+
+ @Test
+ public void testNaGraphqlDelivery() {
+ assertEquals("https://graphql.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "graphqlDelivery"));
+ }
+
+ @Test
+ public void testNaAuth() {
+ assertEquals("https://auth-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "auth"));
+ }
+
+ @Test
+ public void testEuPreview() {
+ assertEquals("https://eu-rest-preview.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "preview"));
+ }
+
+ @Test
+ public void testNaApplication() {
+ assertEquals("https://app.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "application"));
+ }
+
+ @Test
+ public void testNaAssetManagement() {
+ assertEquals("https://am-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("na", "assetManagement"));
+ }
+
+ // ── omitHttps ─────────────────────────────────────────────────────────────
+
+ @Test
+ public void testOmitHttpsNaContentDelivery() {
+ assertEquals("cdn.contentstack.io",
+ Endpoint.getContentstackEndpoint("na", "contentDelivery", true));
+ }
+
+ @Test
+ public void testOmitHttpsEuContentDelivery() {
+ assertEquals("eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery", true));
+ }
+
+ @Test
+ public void testOmitHttpsAzureNaContentManagement() {
+ assertEquals("azure-na-api.contentstack.com",
+ Endpoint.getContentstackEndpoint("azure-na", "contentManagement", true));
+ }
+
+ @Test
+ public void testOmitHttpsFalseReturnsFullUrl() {
+ String url = Endpoint.getContentstackEndpoint("gcp-eu", "contentDelivery", false);
+ assertTrue(url.startsWith("https://"));
+ }
+
+ // ── getAllEndpoints ───────────────────────────────────────────────────────
+
+ @Test
+ public void testGetAllEndpointsNaContainsContentDelivery() {
+ Map endpoints = Endpoint.getAllEndpoints("na");
+ assertTrue(endpoints.containsKey("contentDelivery"));
+ assertEquals("https://cdn.contentstack.io", endpoints.get("contentDelivery"));
+ }
+
+ @Test
+ public void testGetAllEndpointsEuSize() {
+ Map endpoints = Endpoint.getAllEndpoints("eu");
+ assertFalse(endpoints.isEmpty());
+ assertTrue(endpoints.size() >= 4);
+ }
+
+ @Test
+ public void testGetAllEndpointsOmitHttps() {
+ Map endpoints = Endpoint.getAllEndpoints("na", true);
+ for (String url : endpoints.values()) {
+ assertFalse("Expected no https:// prefix but got: " + url, url.startsWith("https://"));
+ }
+ }
+
+ @Test
+ public void testGetAllEndpointsAzureNaOmitHttps() {
+ Map endpoints = Endpoint.getAllEndpoints("azure-na", true);
+ assertEquals("azure-na-cdn.contentstack.com", endpoints.get("contentDelivery"));
+ }
+
+ // ── error cases ───────────────────────────────────────────────────────────
+
+ @Test
+ public void testEmptyRegionThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Endpoint.getContentstackEndpoint("", "contentDelivery"));
+ }
+
+ @Test
+ public void testBlankRegionThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Endpoint.getContentstackEndpoint(" ", "contentDelivery"));
+ }
+
+ @Test
+ public void testUnknownServiceThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Endpoint.getContentstackEndpoint("na", "cms"));
+ }
+
+ @Test
+ public void testServiceNotAvailableInRegionThrows() {
+ // assetManagement exists only in NA
+ assertThrows(IllegalArgumentException.class,
+ () -> Endpoint.getContentstackEndpoint("eu", "assetManagement"));
+ }
+
+ @Test
+ public void testGetAllEndpointsEmptyRegionThrows() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Endpoint.getAllEndpoints(""));
+ }
+
+ // ── caching ───────────────────────────────────────────────────────────────
+
+ @Test
+ public void testMultipleCallsReturnSameResult() {
+ String url1 = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
+ String url2 = Endpoint.getContentstackEndpoint("eu", "contentDelivery");
+ assertEquals(url1, url2);
+ }
+
+ @Test
+ public void testCacheResetAllowsReload() {
+ String url1 = Endpoint.getContentstackEndpoint("na", "contentDelivery");
+ Endpoint.resetCache();
+ String url2 = Endpoint.getContentstackEndpoint("na", "contentDelivery");
+ assertEquals(url1, url2);
+ }
+
+ @Test
+ public void testResetCacheClearsLiveRefreshFlag() {
+ Endpoint.resetCache();
+ String url = Endpoint.getContentstackEndpoint("na", "contentDelivery");
+ assertEquals("https://cdn.contentstack.io", url);
+ }
+
+ // ── proxy-aware resolution ────────────────────────────────────────────────
+
+ @Test
+ public void testProxyOverloadResolvesBundledRegion() {
+ // A bundled region never triggers the live download, so the proxy is unused but the
+ // proxy-aware overload must still resolve correctly.
+ java.net.Proxy proxy = new java.net.Proxy(
+ java.net.Proxy.Type.HTTP, new java.net.InetSocketAddress("127.0.0.1", 8080));
+ assertEquals("eu-cdn.contentstack.com",
+ Endpoint.getContentstackEndpoint("eu", "contentDelivery", true, proxy));
+ }
+}
diff --git a/contentstack/src/test/java/com/contentstack/sdk/TestStack.java b/contentstack/src/test/java/com/contentstack/sdk/TestStack.java
index f6ec85b1..c4ba9a15 100644
--- a/contentstack/src/test/java/com/contentstack/sdk/TestStack.java
+++ b/contentstack/src/test/java/com/contentstack/sdk/TestStack.java
@@ -382,7 +382,7 @@ public void testAzureNaRegionSetsCorrectURL() throws Exception {
@Test
public void testNonUsRegionsSetsCorrectStackURL() throws Exception {
Map expectedHosts = new HashMap<>();
- expectedHosts.put(com.contentstack.sdk.Config.ContentstackRegion.EU, "eu-cdn.contentstack.io");
+ expectedHosts.put(com.contentstack.sdk.Config.ContentstackRegion.EU, "eu-cdn.contentstack.com");
expectedHosts.put(com.contentstack.sdk.Config.ContentstackRegion.AU, "au-cdn.contentstack.com");
expectedHosts.put(com.contentstack.sdk.Config.ContentstackRegion.AZURE_NA, "azure-na-cdn.contentstack.com");
expectedHosts.put(com.contentstack.sdk.Config.ContentstackRegion.AZURE_EU, "azure-eu-cdn.contentstack.com");
diff --git a/scripts/download-regions.sh b/scripts/download-regions.sh
new file mode 100755
index 00000000..0c79d1f8
--- /dev/null
+++ b/scripts/download-regions.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Download the latest regions.json from the Contentstack artifacts registry and
+# write it to contentstack/src/main/resources/assets/regions.json so it gets
+# bundled into the SDK on the next build.
+#
+# Usage:
+# ./scripts/download-regions.sh
+# ./gradlew refreshRegions
+#
+# Run this whenever Contentstack announces new regions or service keys, then
+# commit the updated file:
+# git add contentstack/src/main/resources/assets/regions.json
+# git commit -m "chore: refresh regions.json"
+
+set -euo pipefail
+
+REGIONS_URL="https://artifacts.contentstack.com/regions.json"
+DEST="$(dirname "$0")/../contentstack/src/main/resources/assets/regions.json"
+mkdir -p "$(dirname "$DEST")"
+DEST="$(cd "$(dirname "$DEST")" && pwd)/$(basename "$DEST")"
+
+echo "Downloading regions.json from ${REGIONS_URL} ..."
+
+if command -v curl &>/dev/null; then
+ curl --silent --show-error --fail --location \
+ --retry 3 --retry-delay 2 \
+ -o "${DEST}" "${REGIONS_URL}"
+elif command -v wget &>/dev/null; then
+ wget --quiet --tries=3 --waitretry=2 -O "${DEST}" "${REGIONS_URL}"
+else
+ echo "Error: neither curl nor wget found. Install one and retry." >&2
+ exit 1
+fi
+
+# Validate the downloaded file contains a "regions" array
+if ! python3 -c "import sys, json; d=json.load(open('${DEST}')); assert 'regions' in d and len(d['regions']) > 0" 2>/dev/null &&
+ ! python -c "import sys, json; d=json.load(open('${DEST}')); assert 'regions' in d and len(d['regions']) > 0" 2>/dev/null; then
+ # Fallback validation without Python — just check the key exists
+ if ! grep -q '"regions"' "${DEST}"; then
+ echo "Error: downloaded file does not look like a valid regions.json" >&2
+ rm -f "${DEST}"
+ exit 1
+ fi
+fi
+
+REGION_COUNT=$(grep -o '"id"' "${DEST}" | wc -l | tr -d ' ')
+echo "contentstack-android: regions.json updated (${REGION_COUNT} regions) → ${DEST}"