Skip to content

Commit 42fff2a

Browse files
jamesnroktclaude
andcommitted
fix: persist customBaseURL across NetworkOptions JSON round-trip
NetworkOptions.toJson() and withNetworkOptions(String) did not include customBaseURL, so any value was silently dropped when UploadSettings serialized NetworkOptions to the upload database. Events and alias uploads read back NetworkOptions without customBaseURL and routed to the default mParticle endpoints instead of the partner CNAME. Also: - Extract the customBaseURL/DomainMapping host-resolution branch out of getUrl() into a private resolveHost() helper plus a small ResolvedHost value type, lowering getUrl()'s cyclomatic complexity. - Switch java.net.URL / java.net.MalformedURLException to imports. - Add two androidTest cases covering the round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 96950e7 commit 42fff2a

3 files changed

Lines changed: 96 additions & 40 deletions

File tree

android-core/src/androidTest/kotlin/com.mparticle/networking/MParticleBaseClientImplTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,4 +570,29 @@ class MParticleBaseClientImplTest : BaseCleanInstallEachTest() {
570570
.build()
571571
assertEquals(null, opts.customBaseURL)
572572
}
573+
574+
@Test
575+
fun testCustomBaseURLSurvivesJsonRoundTrip() {
576+
val original =
577+
NetworkOptions
578+
.builder()
579+
.setCustomBaseURL("https://rkt.example.com:8443")
580+
.build()
581+
Assert.assertEquals("rkt.example.com:8443", original.customBaseURL)
582+
583+
val json = original.toJson().toString()
584+
val restored = NetworkOptions.withNetworkOptions(json)
585+
Assert.assertNotNull(restored)
586+
Assert.assertEquals("rkt.example.com:8443", restored!!.customBaseURL)
587+
}
588+
589+
@Test
590+
fun testCustomBaseURLOmittedFromJsonWhenUnset() {
591+
val opts = NetworkOptions.builder().build()
592+
val json = opts.toJson()
593+
Assert.assertFalse(json.has("customBaseURL"))
594+
595+
val restored = NetworkOptions.withNetworkOptions(json.toString())
596+
Assert.assertNull(restored!!.customBaseURL)
597+
}
573598
}

android-core/src/main/java/com/mparticle/networking/MParticleBaseClientImpl.java

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -114,50 +114,18 @@ protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath, HashMap
114114
protected MPUrl getUrl(Endpoint endpoint, @Nullable String identityPath,HashMap<String, String> audienceQueryParams, @Nullable UploadSettings uploadSettings) throws MalformedURLException {
115115
NetworkOptions networkOptions = uploadSettings == null ? mConfigManager.getNetworkOptions() : uploadSettings.getNetworkOptions();
116116
DomainMapping domainMapping = networkOptions.getDomain(endpoint);
117-
String url = NetworkOptionsManager.getDefaultUrl(endpoint);
118117
String apiKey = uploadSettings == null ? mApiKey : uploadSettings.getApiKey();
119-
final String customBaseURL = networkOptions.getCustomBaseURL();
118+
final boolean usingCustomBaseURL = !MPUtility.isEmpty(networkOptions.getCustomBaseURL());
120119

121-
// `defaultDomain` variable is for URL generation when domain mapping is specified.
122-
String defaultDomain = url;
123-
boolean isDefaultDomain = true;
124-
125-
if (!MPUtility.isEmpty(customBaseURL)) {
126-
// customBaseURL takes priority over all individual domain mappings.
127-
if (domainMapping != null && !MPUtility.isEmpty(domainMapping.getUrl())) {
128-
Logger.warning("NetworkOptions: customBaseURL is set; domain mapping for " + endpoint.name() + " is ignored.");
129-
}
130-
url = customBaseURL;
131-
isDefaultDomain = false;
132-
if (endpoint != Endpoint.CONFIG) {
133-
// When custom CNAME is used, the default-domain URL still needs the pod prefix
134-
// so MPConnectionTest matching and pinning fallbacks continue to work.
135-
defaultDomain = getPodUrl(defaultDomain, null, false);
136-
}
137-
} else {
138-
// Check if domain mapping is specified and update the URL based on domain mapping
139-
String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null;
140-
if (!MPUtility.isEmpty(domainMappingUrl)) {
141-
isDefaultDomain = url.equals(domainMappingUrl);
142-
url = domainMappingUrl;
143-
}
144-
145-
if (endpoint != Endpoint.CONFIG) {
146-
// Set URL with pod prefix if it’s the default domain and endpoint is not CONFIG
147-
if (isDefaultDomain) {
148-
url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled());
149-
} else {
150-
// When domain mapping is specified, generate the default domain. Whether podRedirection is enabled or not, always use the original URL.
151-
defaultDomain = getPodUrl(defaultDomain, null, false);
152-
}
153-
}
154-
}
120+
ResolvedHost host = resolveHost(endpoint, networkOptions, domainMapping, apiKey);
121+
String url = host.url;
122+
String defaultDomain = host.defaultDomain;
123+
boolean isDefaultDomain = host.isDefaultDomain;
155124

156125
Uri uri;
157126
String subdirectory;
158127
String pathPrefix;
159128
String pathPostfix;
160-
final boolean usingCustomBaseURL = !MPUtility.isEmpty(customBaseURL);
161129
boolean overridesSubdirectory = domainMapping != null && domainMapping.isOverridesSubdirectory();
162130
if (usingCustomBaseURL && overridesSubdirectory) {
163131
Logger.warning("NetworkOptions: customBaseURL with overridesSubdirectory is unsupported for CDN routing; overridesSubdirectory will be ignored for " + endpoint.name() + ".");
@@ -268,6 +236,57 @@ String getPodUrl(String URLPrefix, String pod, boolean enablePodRedirection) {
268236
return null;
269237
}
270238

239+
/**
240+
* Resolves the host(s) used to build the endpoint URL. Returns three values:
241+
* {@code url} — host used for the request, {@code defaultDomain} — host used as the
242+
* fallback for {@link #generateDefaultURL}, and {@code isDefaultDomain} — true when
243+
* {@code url} is the unmodified mParticle default.
244+
*
245+
* <p>Priority: {@code customBaseURL} → per-endpoint {@code DomainMapping} → default.
246+
*/
247+
private ResolvedHost resolveHost(Endpoint endpoint, NetworkOptions networkOptions, DomainMapping domainMapping, String apiKey) {
248+
String defaultUrl = NetworkOptionsManager.getDefaultUrl(endpoint);
249+
String customBaseURL = networkOptions.getCustomBaseURL();
250+
251+
if (!MPUtility.isEmpty(customBaseURL)) {
252+
if (domainMapping != null && !MPUtility.isEmpty(domainMapping.getUrl())) {
253+
Logger.warning("NetworkOptions: customBaseURL is set; domain mapping for " + endpoint.name() + " is ignored.");
254+
}
255+
// When custom CNAME is used, the default-domain URL still needs the pod prefix
256+
// so MPConnectionTest matching and pinning fallbacks continue to work.
257+
String defaultDomain = endpoint == Endpoint.CONFIG ? defaultUrl : getPodUrl(defaultUrl, null, false);
258+
return new ResolvedHost(customBaseURL, defaultDomain, false);
259+
}
260+
261+
String domainMappingUrl = domainMapping != null ? domainMapping.getUrl() : null;
262+
boolean isDefaultDomain = MPUtility.isEmpty(domainMappingUrl) || defaultUrl.equals(domainMappingUrl);
263+
String url = isDefaultDomain ? defaultUrl : domainMappingUrl;
264+
String defaultDomain = defaultUrl;
265+
266+
if (endpoint != Endpoint.CONFIG) {
267+
if (isDefaultDomain) {
268+
// Default domain gets the pod prefix.
269+
url = getPodUrl(url, mConfigManager.getPodPrefix(apiKey), mConfigManager.isDirectUrlRoutingEnabled());
270+
} else {
271+
// Domain-mapped: always generate the default with the original (un-pod-prefixed) host.
272+
defaultDomain = getPodUrl(defaultDomain, null, false);
273+
}
274+
}
275+
return new ResolvedHost(url, defaultDomain, isDefaultDomain);
276+
}
277+
278+
private static final class ResolvedHost {
279+
final String url;
280+
final String defaultDomain;
281+
final boolean isDefaultDomain;
282+
283+
ResolvedHost(String url, String defaultDomain, boolean isDefaultDomain) {
284+
this.url = url;
285+
this.defaultDomain = defaultDomain;
286+
this.isDefaultDomain = isDefaultDomain;
287+
}
288+
}
289+
271290
public enum Endpoint {
272291
CONFIG(1),
273292
IDENTITY(2),

android-core/src/main/java/com/mparticle/networking/NetworkOptions.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import org.json.JSONException;
1717
import org.json.JSONObject;
1818

19+
import java.net.MalformedURLException;
20+
import java.net.URL;
1921
import java.util.ArrayList;
2022
import java.util.HashMap;
23+
import java.util.HashSet;
2124
import java.util.List;
2225
import java.util.Map;
23-
import java.util.HashSet;
2426
import java.util.Set;
2527

2628
public class NetworkOptions {
@@ -66,6 +68,13 @@ public static NetworkOptions withNetworkOptions(@Nullable String jsonString) {
6668
JSONObject jsonObject = new JSONObject(jsonString);
6769
builder.setPinningDisabledInDevelopment(jsonObject.optBoolean("disableDevPinning", false));
6870
builder.setPinningDisabled(jsonObject.optBoolean("disablePinning", false));
71+
String storedCustomBaseURL = jsonObject.optString("customBaseURL", null);
72+
if (!MPUtility.isEmpty(storedCustomBaseURL)) {
73+
// Stored value is already host(:port). Skip the Builder setter (which expects a full
74+
// HTTPS URL with scheme) and assign directly so a previously-validated value survives
75+
// the JSON round-trip.
76+
builder.customBaseURL = storedCustomBaseURL;
77+
}
6978
JSONArray domainMappingsJson = jsonObject.getJSONArray("domainMappings");
7079
for (int i = 0; i < domainMappingsJson.length(); i++) {
7180
builder.addDomainMapping(DomainMapping
@@ -138,6 +147,9 @@ public JSONObject toJson() {
138147
JSONArray domainMappingsJson = new JSONArray();
139148
networkOptions.put("disableDevPinning", pinningDisabledInDevelopment);
140149
networkOptions.put("disablePinning", pinningDisabled);
150+
if (!MPUtility.isEmpty(customBaseURL)) {
151+
networkOptions.put("customBaseURL", customBaseURL);
152+
}
141153
networkOptions.put("domainMappings", domainMappingsJson);
142154
for (DomainMapping domainMapping : domainMappings.values()) {
143155
domainMappingsJson.put(domainMapping.toString());
@@ -221,7 +233,7 @@ public Builder setPinningDisabled(boolean disabled) {
221233
@NonNull
222234
public Builder setCustomBaseURL(@NonNull String customBaseURL) {
223235
try {
224-
java.net.URL parsed = new java.net.URL(customBaseURL);
236+
URL parsed = new URL(customBaseURL);
225237
if (!"https".equalsIgnoreCase(parsed.getProtocol()) || MPUtility.isEmpty(parsed.getHost())) {
226238
Logger.warning("NetworkOptions: customBaseURL must use HTTPS and include a valid host — value ignored.");
227239
return this;
@@ -231,7 +243,7 @@ public Builder setCustomBaseURL(@NonNull String customBaseURL) {
231243
host.append(":").append(parsed.getPort());
232244
}
233245
this.customBaseURL = host.toString();
234-
} catch (java.net.MalformedURLException e) {
246+
} catch (MalformedURLException e) {
235247
Logger.warning("NetworkOptions: customBaseURL is malformed — value ignored.");
236248
}
237249
return this;

0 commit comments

Comments
 (0)