diff --git a/Makefile b/Makefile
index ed9965b7..05f23b0a 100644
--- a/Makefile
+++ b/Makefile
@@ -21,8 +21,8 @@ run-contract-tests:
| VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh
@echo "Running SDK contract test v3..."
@curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \
- -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.1.0-alpha.6/downloader/run.sh \
- | VERSION=v3.1.0-alpha.6 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh
+ -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.2.0-alpha.10/downloader/run.sh \
+ | VERSION=v3.2.0-alpha.10 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh
contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests
diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java
index 4b7aa650..5c94b580 100644
--- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java
+++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java
@@ -78,6 +78,7 @@ public static class SdkConfigClientSideParams {
LDContext initialContext;
boolean evaluationReasons;
boolean useReport;
+ boolean usePost;
boolean includeEnvironmentAttributes;
}
diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java
index f9627704..1e35a31d 100644
--- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java
+++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java
@@ -284,7 +284,7 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,
ConfigHelper.configureIsolatedInMemoryPersistence(builder);
if (params.dataSystem != null) {
- configureDataSystem(builder, params.dataSystem);
+ configureDataSystem(builder, params.dataSystem, params.clientSide.usePost);
} else {
if (params.polling != null && params.polling.baseUri != null) {
endpoints.polling(params.polling.baseUri);
@@ -391,15 +391,16 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,
return builder.build();
}
- private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem) {
+ private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem,
+ boolean usePost) {
if (Boolean.TRUE.equals(dataSystem.useDefaultDataSystem)) {
- builder.dataSystem(Components.dataSystem());
+ builder.dataSystem(Components.dataSystem().usePost(usePost));
return;
}
SdkConfigConnectionModeConfig connModeConfig = dataSystem.connectionModeConfig;
- DataSystemBuilder dsBuilder = Components.dataSystem();
+ DataSystemBuilder dsBuilder = Components.dataSystem().usePost(usePost);
// at the time of writing this, we did not have contract tests that could test platform state changes,
// disabling automatic mode simplifies the behavior being tested
diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java
index b6618d9e..105006a6 100644
--- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java
+++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java
@@ -43,7 +43,8 @@ public class TestService extends NanoHTTPD {
"evaluation-hooks",
"track-hooks",
"client-per-context-summaries",
- "client-event-source-http-errors"
+ "client-event-source-http-errors",
+ "client-use-post"
};
private static final String MIME_JSON = "application/json";
static final Gson gson = new GsonBuilder()
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternal.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternal.java
index 1299859b..16dc8210 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternal.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternal.java
@@ -35,13 +35,14 @@ final class DataSourceBuildInputsInternal extends DataSourceBuildInputs {
ServiceEndpoints serviceEndpoints,
HttpConfiguration http,
boolean evaluationReasons,
+ boolean usePost,
SelectorSource selectorSource,
ScheduledExecutorService sharedExecutor,
@NonNull File cacheDir,
LDLogger baseLogger,
@Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData perEnvironmentData
) {
- super(evaluationContext, serviceEndpoints, http, evaluationReasons,
+ super(evaluationContext, serviceEndpoints, http, evaluationReasons, usePost,
selectorSource, sharedExecutor, cacheDir, baseLogger);
this.perEnvironmentData = perEnvironmentData;
}
@@ -60,6 +61,7 @@ static DataSourceBuildInputsInternal get(DataSourceBuildInputs inputs) {
inputs.getServiceEndpoints(),
inputs.getHttp(),
inputs.isEvaluationReasons(),
+ inputs.isUsePost(),
inputs.getSelectorSource(),
inputs.getSharedExecutor(),
inputs.getCacheDir(),
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java
index 4d9dfea8..652488f6 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java
@@ -141,7 +141,7 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH,
requestor,
initialReconnectDelayMillis,
- inputs.isEvaluationReasons(), inputs.getHttp().isUseReport(),
+ inputs.isEvaluationReasons(), inputs.isUsePost(),
httpProps, inputs.getSharedExecutor(),
inputs.getBaseLogger(), null);
}
@@ -159,10 +159,12 @@ public FDv1PollingSynchronizerBuilderImpl pollIntervalMillis(int pollIntervalMil
@Override
public Synchronizer build(DataSourceBuildInputs inputs) {
+ // The FDv1 endpoints do not accept POST. REPORT is their method that carries the
+ // context in the request body, so usePost selects REPORT here.
FeatureFetcher fetcher = new HttpFeatureFlagFetcher(
inputs.getServiceEndpoints().getPollingBaseUri(),
inputs.isEvaluationReasons(),
- inputs.getHttp().isUseReport(),
+ inputs.isUsePost(),
LDUtil.makeHttpProperties(inputs.getHttp()),
inputs.getCacheDir(),
inputs.getBaseLogger()
@@ -360,9 +362,8 @@ private static FDv2Requestor makePollingRequestor(DataSourceBuildInputs inputs,
"polling", inputs.getBaseLogger());
return new DefaultFDv2Requestor(
inputs.getEvaluationContext(), pollingBase,
- StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH,
- StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH,
- httpProps, inputs.getHttp().isUseReport(),
+ StandardEndpoints.FDV2_POLLING_REQUEST_BASE_PATH,
+ httpProps, inputs.isUsePost(),
inputs.isEvaluationReasons(), null, inputs.getBaseLogger());
}
}
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultFDv2Requestor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultFDv2Requestor.java
index 2ffa79e8..0a554ee5 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultFDv2Requestor.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultFDv2Requestor.java
@@ -31,7 +31,7 @@
/**
* Default OkHttp-based implementation of {@link FDv2Requestor}.
*
- * Builds GET or REPORT requests to the FDv2 polling endpoint. If the current selector is
+ * Builds GET or POST requests to the FDv2 polling endpoint. If the current selector is
* non-empty, its state is sent as the {@code basis} query parameter. ETag tracking is used
* to detect 304 Not Modified responses; when the server returns 304 the application layer
* treats it as {@code ChangeSetType.None} (no flags changed), so no OkHttp disk cache is
@@ -40,7 +40,6 @@
* The OkHttpClient is closed by {@link #close()}.
*/
final class DefaultFDv2Requestor implements FDv2Requestor {
- private static final String METHOD_REPORT = "REPORT";
private static final String BASIS_PARAM = "basis";
private static final String FILTER_PARAM = "filter";
private static final String WITH_REASONS_PARAM = "withReasons";
@@ -50,10 +49,10 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
private final OkHttpClient httpClient;
private final URI pollingUri;
private final okhttp3.Headers headers;
- private final boolean useReport;
+ private final boolean usePost;
private final boolean evaluationReasons;
@Nullable
- private final RequestBody reportBody;
+ private final RequestBody postBody;
@Nullable
private final String payloadFilter;
private final LDLogger logger;
@@ -67,10 +66,10 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
/**
* @param evaluationContext the context to evaluate flags for
* @param baseUri polling base URI from service endpoints
- * @param getRequestPath path for GET requests (context appended as a path segment)
- * @param reportRequestPath path for REPORT requests (context sent in the request body)
+ * @param requestPath polling request path; GET appends the base64-encoded context as a
+ * path segment, POST uses the path as is
* @param httpProperties SDK HTTP configuration (timeouts, proxy, TLS, user-agent, etc.)
- * @param useReport if true, send context in the request body via REPORT; otherwise
+ * @param usePost if true, send the context in the request body via POST; otherwise
* append the base64-encoded context to the GET path
* @param evaluationReasons if true, append {@code withReasons=true} to the query string
* @param payloadFilter optional payload filter key; sent as {@code filter} query param
@@ -79,14 +78,13 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
DefaultFDv2Requestor(
@NonNull LDContext evaluationContext,
@NonNull URI baseUri,
- @NonNull String getRequestPath,
- @NonNull String reportRequestPath,
+ @NonNull String requestPath,
@NonNull HttpProperties httpProperties,
- boolean useReport,
+ boolean usePost,
boolean evaluationReasons,
@Nullable String payloadFilter,
@NonNull LDLogger logger) {
- this.useReport = useReport;
+ this.usePost = usePost;
this.evaluationReasons = evaluationReasons;
this.payloadFilter = payloadFilter;
this.logger = logger;
@@ -94,14 +92,13 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
// Precompute the base polling URI for the chosen request method. For GET, the
// base64-encoded context is a fixed path segment (context never changes after
- // construction). For REPORT, the context goes in the request body so no path
+ // construction). For POST, the context goes in the request body so no path
// segment is needed.
- URI basePollingUri = HttpHelpers.concatenateUriPath(baseUri,
- useReport ? reportRequestPath : getRequestPath);
- this.pollingUri = useReport
+ URI basePollingUri = HttpHelpers.concatenateUriPath(baseUri, requestPath);
+ this.pollingUri = usePost
? basePollingUri
: HttpHelpers.concatenateUriPath(basePollingUri, LDUtil.urlSafeBase64(evaluationContext));
- this.reportBody = useReport
+ this.postBody = usePost
? RequestBody.create(JsonSerialization.serialize(evaluationContext), JSON)
: null;
@@ -139,8 +136,8 @@ public Future poll(@NonNull Selector selector) {
.url(requestUri.toURL())
.headers(headers);
- if (useReport) {
- reqBuilder.method(METHOD_REPORT, reportBody);
+ if (usePost) {
+ reqBuilder.post(postBody);
} else {
synchronized (etagLock) {
if (!requestUri.equals(lastRequestUri)) {
@@ -207,7 +204,7 @@ private void handleResponse(
return;
}
- if (!useReport) {
+ if (!usePost) {
synchronized (etagLock) {
cachedEtag = response.header(ETAG_HEADER);
}
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java
index 769eaa31..5d90b6ea 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java
@@ -34,10 +34,13 @@ class FDv2DataSourceBuilder implements ComponentConfigurer, Closeabl
private final Map modeTable;
private final ConnectionMode startingMode;
private final ModeResolutionTable resolutionTable;
+ private final boolean usePost;
private ConnectionMode activeMode;
private boolean includeInitializers = true; // start with initializers
private ScheduledExecutorService sharedExecutor;
+ // The useReport warning is logged on the first build only. Builds run on one thread.
+ private boolean useReportWarningLogged = false;
FDv2DataSourceBuilder() {
this(DataSystemComponents.makeDefaultModeTable(), ConnectionMode.STREAMING, ModeResolutionTable.MOBILE);
@@ -54,10 +57,24 @@ class FDv2DataSourceBuilder implements ComponentConfigurer, Closeabl
@NonNull Map modeTable,
@NonNull ConnectionMode startingMode,
@NonNull ModeResolutionTable resolutionTable
+ ) {
+ this(modeTable, startingMode, resolutionTable, false);
+ }
+
+ /**
+ * @param usePost true if FDv2 polling and streaming requests send the context in the request
+ * body with POST instead of encoding it into the GET path
+ */
+ FDv2DataSourceBuilder(
+ @NonNull Map modeTable,
+ @NonNull ConnectionMode startingMode,
+ @NonNull ModeResolutionTable resolutionTable,
+ boolean usePost
) {
this.modeTable = modeTable;
this.startingMode = startingMode;
this.resolutionTable = resolutionTable;
+ this.usePost = usePost;
}
/**
@@ -75,6 +92,15 @@ ConnectionMode getStartingMode() {
return startingMode;
}
+ /**
+ * Returns whether FDv2 flag requests send the context in the request body with POST.
+ *
+ * @return true if flag requests use POST
+ */
+ boolean isUsePost() {
+ return usePost;
+ }
+
/**
* Configures the mode to build for and whether to include initializers.
* Called by {@link ConnectivityManager} before each {@link #build} call.
@@ -108,6 +134,15 @@ public DataSource build(ClientContext clientContext) {
ConnectionMode mode = activeMode != null ? activeMode : startingMode;
+ // The FDv2 endpoints take POST, not REPORT. The data system has its own usePost option,
+ // so the FDv1 useReport option does not apply here.
+ if (clientContext.getHttp().isUseReport() && !useReportWarningLogged) {
+ useReportWarningLogged = true;
+ clientContext.getBaseLogger().warn(
+ "useReport has no effect with the FDv2 data system and is ignored. " +
+ "Use DataSystemBuilder.usePost to send the evaluation context in the request body.");
+ }
+
ModeDefinition modeDef = modeTable.get(mode);
if (modeDef == null) {
throw new IllegalStateException(
@@ -159,6 +194,7 @@ private DataSourceBuildInputsInternal makeInputs(ClientContext clientContext) {
clientContext.getServiceEndpoints(),
clientContext.getHttp(),
clientContext.isEvaluationReasons(),
+ usePost,
selectorSource,
sharedExecutor,
impl.getPlatformState().getCacheDir(),
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizer.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizer.java
index ef8e0d5f..756d3792 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizer.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizer.java
@@ -53,7 +53,7 @@
*
*/
final class FDv2StreamingSynchronizer implements Synchronizer {
- private static final String METHOD_REPORT = "REPORT";
+ private static final String METHOD_POST = "POST";
private static final String PING = "ping";
private static final long READ_TIMEOUT_MS = 300_000; // 5 minutes
private static final long MAX_RECONNECT_TIME_MS = 300_000; // 5 minutes
@@ -61,7 +61,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
private final HttpProperties httpProperties;
private final URI streamBaseUri;
private final String streamRequestPath;
- private final boolean useReport;
+ private final boolean usePost;
private final LDContext evaluationContext;
private final SelectorSource selectorSource;
@Nullable
@@ -92,7 +92,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
* @param requestor optional requestor for handling ping events via poll; may be null
* @param initialReconnectDelayMillis delay before reconnecting after an error, in milliseconds
* @param evaluationReasons true to request evaluation reasons in the stream
- * @param useReport true to use HTTP REPORT for the request body
+ * @param usePost true to send the context in the request body with POST
* @param httpProperties HTTP configuration for the stream request
* @param executor executor used to run the streaming loop on a background
* thread; should use background-priority threads
@@ -107,7 +107,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
@Nullable FDv2Requestor requestor,
int initialReconnectDelayMillis,
boolean evaluationReasons,
- boolean useReport,
+ boolean usePost,
@NonNull HttpProperties httpProperties,
@NonNull Executor executor,
@NonNull LDLogger logger,
@@ -120,7 +120,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
this.requestor = requestor;
this.initialReconnectDelayMillis = initialReconnectDelayMillis;
this.evaluationReasons = evaluationReasons;
- this.useReport = useReport;
+ this.usePost = usePost;
this.httpProperties = httpProperties;
this.executor = executor;
this.logger = logger;
@@ -188,9 +188,9 @@ private void startStream() {
return reqBuilder.build();
});
- if (useReport) {
+ if (usePost) {
connectStrategy = connectStrategy.methodAndBody(
- METHOD_REPORT,
+ METHOD_POST,
RequestBody.create(JsonSerialization.serialize(evaluationContext), JSON));
}
@@ -244,7 +244,7 @@ private void startStream() {
private URI getStreamUri() {
URI uri = HttpHelpers.concatenateUriPath(streamBaseUri, streamRequestPath);
- if (!useReport) {
+ if (!usePost) {
uri = HttpHelpers.concatenateUriPath(uri, LDUtil.urlSafeBase64(evaluationContext));
}
if (evaluationReasons) {
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java
index 9056f9dc..bb2026d6 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java
@@ -814,7 +814,7 @@ public LDConfig build() {
ModeResolutionTable resolutionTable = ModeResolutionTable.createMobile(
startingMode, backgroundMode);
effectiveDataSource = new FDv2DataSourceBuilder(
- modeTable, startingMode, resolutionTable);
+ modeTable, startingMode, resolutionTable, this.dataSystemBuilder.isUsePost());
effectiveAutoModeSwitching = this.dataSystemBuilder.getAutomaticModeSwitchingConfig();
} else {
effectiveDataSource = this.dataSource == null
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java
index 06c51e32..a0b415b7 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java
@@ -17,8 +17,8 @@ private StandardEndpoints() {}
static final String ANALYTICS_EVENTS_REQUEST_PATH = "/mobile/events/bulk";
static final String DIAGNOSTIC_EVENTS_REQUEST_PATH = "/mobile/events/diagnostic";
- static final String FDV2_POLLING_REQUEST_GET_BASE_PATH = "/sdk/poll/eval";
- static final String FDV2_POLLING_REQUEST_REPORT_BASE_PATH = "/sdk/poll/eval";
+ // GET appends the base64url context as a path segment; POST sends it in the body.
+ static final String FDV2_POLLING_REQUEST_BASE_PATH = "/sdk/poll/eval";
static final String FDV2_STREAMING_REQUEST_BASE_PATH = "/sdk/stream/eval";
/**
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java
index a96e96a4..e2b24228 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java
@@ -89,6 +89,7 @@ public class DataSystemBuilder {
private ConnectionMode foregroundConnectionMode = ConnectionMode.STREAMING;
private ConnectionMode backgroundConnectionMode = ConnectionMode.BACKGROUND;
private AutomaticModeSwitchingConfig automaticModeSwitchingConfig = AutomaticModeSwitchingConfig.enabled();
+ private boolean usePost = false;
private final Map connectionModeOverrides = new LinkedHashMap<>();
/**
@@ -160,6 +161,30 @@ public DataSystemBuilder automaticModeSwitching(@NonNull AutomaticModeSwitchingC
return this;
}
+ /**
+ * Sets whether polling and streaming flag requests send the evaluation context in the
+ * request body with the HTTP POST method.
+ *
+ * By default the SDK uses GET and encodes the context into the request path. POST keeps
+ * the context out of the URL, so it does not appear in request logs, and it avoids URL
+ * length limits for large contexts.
+ *
+ * When the SDK falls back to the FDv1 endpoints, the context is sent in the request body
+ * with the REPORT method, which is the body-carrying method those endpoints accept.
+ *
+ * This option replaces {@link HttpConfigurationBuilder#useReport(boolean)} for the data
+ * system. The {@code useReport} option has no effect when a data system is configured.
+ *
+ * The default is {@code false}.
+ *
+ * @param usePost true to send flag requests with POST
+ * @return this builder
+ */
+ public DataSystemBuilder usePost(boolean usePost) {
+ this.usePost = usePost;
+ return this;
+ }
+
/**
* Overrides the data pipeline for a specific connection mode.
*
@@ -221,6 +246,15 @@ public AutomaticModeSwitchingConfig getAutomaticModeSwitchingConfig() {
return automaticModeSwitchingConfig;
}
+ /**
+ * Returns whether flag requests send the evaluation context in the request body with POST.
+ *
+ * @return true if flag requests use POST
+ */
+ public boolean isUsePost() {
+ return usePost;
+ }
+
/**
* Returns any user-specified mode overrides.
*
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java
index d59f1680..f353e5f0 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java
@@ -92,6 +92,10 @@ public HttpConfigurationBuilder headerTransform(LDHeaderUpdater headerTransform)
* However, the REPORT method is not always supported by operating systems or network gateways.
* Therefore it is disabled in the SDK by default. You can enable it if you know your code will
* not be running in an environment that disallows REPORT.
+ *
+ * This option has no effect when the data system is configured with
+ * {@link com.launchdarkly.sdk.android.LDConfig.Builder#dataSystem(DataSystemBuilder)}. Use
+ * {@link DataSystemBuilder#usePost(boolean)} instead to send the context in the request body.
*
* @param useReport true to enable the REPORT method
* @return the builder
diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceBuildInputs.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceBuildInputs.java
index 14843b40..3e3290e7 100644
--- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceBuildInputs.java
+++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceBuildInputs.java
@@ -28,6 +28,7 @@ public class DataSourceBuildInputs {
private final ServiceEndpoints serviceEndpoints;
private final HttpConfiguration http;
private final boolean evaluationReasons;
+ private final boolean usePost;
private final SelectorSource selectorSource;
private final ScheduledExecutorService sharedExecutor;
private final File cacheDir;
@@ -40,6 +41,7 @@ public class DataSourceBuildInputs {
* @param serviceEndpoints the service endpoint URIs
* @param http the HTTP configuration
* @param evaluationReasons whether evaluation reasons are enabled
+ * @param usePost whether flag requests send the context in the request body with POST
* @param selectorSource the source for obtaining the current selector
* @param sharedExecutor shared executor for scheduling tasks; owned and shut down by
* the calling data source, so components must not shut it down
@@ -51,6 +53,7 @@ public DataSourceBuildInputs(
ServiceEndpoints serviceEndpoints,
HttpConfiguration http,
boolean evaluationReasons,
+ boolean usePost,
SelectorSource selectorSource,
ScheduledExecutorService sharedExecutor,
@NonNull File cacheDir,
@@ -60,6 +63,7 @@ public DataSourceBuildInputs(
this.serviceEndpoints = serviceEndpoints;
this.http = http;
this.evaluationReasons = evaluationReasons;
+ this.usePost = usePost;
this.selectorSource = selectorSource;
this.sharedExecutor = sharedExecutor;
this.cacheDir = cacheDir;
@@ -102,6 +106,17 @@ public boolean isEvaluationReasons() {
return evaluationReasons;
}
+ /**
+ * Returns whether polling and streaming flag requests send the evaluation context in the
+ * request body with the POST method. When false, the context is base64url-encoded into the
+ * request path and the GET method is used.
+ *
+ * @return true if flag requests use POST
+ */
+ public boolean isUsePost() {
+ return usePost;
+ }
+
/**
* Returns the selector source for obtaining the current FDv2 selector.
*
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternalTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternalTest.java
index 6ea14957..9fe16454 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternalTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSourceBuildInputsInternalTest.java
@@ -32,7 +32,7 @@ private static DataSourceBuildInputsInternal makeInternalInputs(
PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData envData
) {
return new DataSourceBuildInputsInternal(
- CONTEXT, null, null, false,
+ CONTEXT, null, null, false, false,
() -> Selector.EMPTY, null, CACHE_DIR,
LDLogger.none(), envData
);
@@ -40,7 +40,7 @@ private static DataSourceBuildInputsInternal makeInternalInputs(
private static DataSourceBuildInputs makePlainInputs() {
return new DataSourceBuildInputs(
- CONTEXT, null, null, false,
+ CONTEXT, null, null, false, false,
() -> Selector.EMPTY, null, CACHE_DIR,
LDLogger.none()
);
@@ -72,12 +72,25 @@ public void get_withPlainInputs_preservesBaseProperties() {
assertEquals(plain.getServiceEndpoints(), result.getServiceEndpoints());
assertEquals(plain.getHttp(), result.getHttp());
assertEquals(plain.isEvaluationReasons(), result.isEvaluationReasons());
+ assertEquals(plain.isUsePost(), result.isUsePost());
assertEquals(plain.getSelectorSource(), result.getSelectorSource());
assertEquals(plain.getSharedExecutor(), result.getSharedExecutor());
assertEquals(plain.getCacheDir(), result.getCacheDir());
assertEquals(plain.getBaseLogger(), result.getBaseLogger());
}
+ @Test
+ public void get_withPlainInputs_preservesUsePost() {
+ DataSourceBuildInputs plain = new DataSourceBuildInputs(
+ CONTEXT, null, null, false, true,
+ () -> Selector.EMPTY, null, CACHE_DIR,
+ LDLogger.none()
+ );
+ DataSourceBuildInputsInternal result = DataSourceBuildInputsInternal.get(plain);
+
+ assertTrue(result.isUsePost());
+ }
+
// ---- getPerEnvironmentDataIfAvailable() ----
@Test
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java
index 0195ec4c..3ec823c5 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemBuilderTest.java
@@ -1,6 +1,7 @@
package com.launchdarkly.sdk.android;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
@@ -26,6 +27,17 @@ public void defaultBuilder_hasExpectedConnectionModesAndAutoSwitching() {
assertTrue(b.getConnectionModeOverrides().isEmpty());
}
+ @Test
+ public void defaultBuilder_usePostIsFalse() {
+ assertFalse(Components.dataSystem().isUsePost());
+ }
+
+ @Test
+ public void usePost_setsFlag() {
+ assertTrue(Components.dataSystem().usePost(true).isUsePost());
+ assertFalse(Components.dataSystem().usePost(true).usePost(false).isUsePost());
+ }
+
@Test
public void buildModeTable_containsAllStandardModes() {
Map table = Components.dataSystem().buildModeTable(false);
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java
new file mode 100644
index 00000000..f7ee5665
--- /dev/null
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DataSystemComponentsTest.java
@@ -0,0 +1,204 @@
+package com.launchdarkly.sdk.android;
+
+import com.launchdarkly.logging.LDLogger;
+import com.launchdarkly.sdk.LDContext;
+import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints;
+import com.launchdarkly.sdk.android.subsystems.DataSourceBuildInputs;
+import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
+import com.launchdarkly.sdk.android.subsystems.Initializer;
+import com.launchdarkly.sdk.android.subsystems.Synchronizer;
+import com.launchdarkly.sdk.fdv2.Selector;
+import com.launchdarkly.testhelpers.httptest.Handlers;
+import com.launchdarkly.testhelpers.httptest.HttpServer;
+import com.launchdarkly.testhelpers.httptest.RequestInfo;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.Timeout;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Verifies that the built-in FDv2 data source builders wire the build inputs into the
+ * requests they send: the request method, the request path, and where the context travels.
+ */
+public class DataSystemComponentsTest {
+
+ @Rule
+ public Timeout globalTimeout = Timeout.seconds(15);
+
+ private static final LDContext CONTEXT = LDContext.create("wiring-test-context-key");
+ private static final File CACHE_DIR = new File(System.getProperty("java.io.tmpdir"));
+
+ private static final String FDV2_POLL_RESPONSE_JSON =
+ "{\"events\":[" +
+ "{\"event\":\"server-intent\",\"data\":{\"payloads\":[{\"id\":\"p1\",\"target\":100,\"intentCode\":\"xfer-full\",\"reason\":\"\"}]}}," +
+ "{\"event\":\"payload-transferred\",\"data\":{\"state\":\"(p:p1:100)\",\"version\":100}}" +
+ "]}";
+
+ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
+
+ @After
+ public void tearDown() {
+ executor.shutdownNow();
+ }
+
+ private DataSourceBuildInputs makeInputs(HttpServer server, boolean usePost) {
+ ServiceEndpoints endpoints = Components.serviceEndpoints()
+ .polling(server.getUri())
+ .streaming(server.getUri())
+ .events(server.getUri())
+ .createServiceEndpoints();
+ HttpConfiguration http = new HttpConfiguration(10_000, Collections.emptyMap(), null, false);
+ return new DataSourceBuildInputs(
+ CONTEXT, endpoints, http, false, usePost,
+ () -> Selector.EMPTY, executor, CACHE_DIR, LDLogger.none());
+ }
+
+ private static String contextPathSegment() {
+ return "/" + LDUtil.urlSafeBase64(CONTEXT);
+ }
+
+ private static void assertContextInBody(RequestInfo request) {
+ assertNotNull("body should contain the serialized context", request.getBody());
+ assertTrue("body should contain the context key",
+ request.getBody().contains("wiring-test-context-key"));
+ }
+
+ private static void assertNoBody(RequestInfo request) {
+ assertTrue("request should have no body",
+ request.getBody() == null || request.getBody().isEmpty());
+ }
+
+ // ---- polling initializer ----
+
+ @Test
+ public void pollingInitializer_defaultUsesGetWithContextInPath() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson(FDV2_POLL_RESPONSE_JSON))) {
+ Initializer initializer = new DataSystemComponents.PollingInitializerBuilderImpl()
+ .build(makeInputs(server, false));
+ initializer.run().get(5, TimeUnit.SECONDS);
+ initializer.close();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ assertEquals("GET", request.getMethod());
+ assertEquals(StandardEndpoints.FDV2_POLLING_REQUEST_BASE_PATH + contextPathSegment(),
+ request.getPath());
+ assertNoBody(request);
+ }
+ }
+
+ @Test
+ public void pollingInitializer_usePostSendsContextInBody() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson(FDV2_POLL_RESPONSE_JSON))) {
+ Initializer initializer = new DataSystemComponents.PollingInitializerBuilderImpl()
+ .build(makeInputs(server, true));
+ initializer.run().get(5, TimeUnit.SECONDS);
+ initializer.close();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ assertEquals("POST", request.getMethod());
+ assertEquals(StandardEndpoints.FDV2_POLLING_REQUEST_BASE_PATH, request.getPath());
+ assertContextInBody(request);
+ }
+ }
+
+ // ---- polling synchronizer ----
+
+ @Test
+ public void pollingSynchronizer_usePostSendsContextInBody() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson(FDV2_POLL_RESPONSE_JSON))) {
+ Synchronizer synchronizer = new DataSystemComponents.PollingSynchronizerBuilderImpl()
+ .build(makeInputs(server, true));
+ synchronizer.next().get(5, TimeUnit.SECONDS);
+ synchronizer.close();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ assertEquals("POST", request.getMethod());
+ assertEquals(StandardEndpoints.FDV2_POLLING_REQUEST_BASE_PATH, request.getPath());
+ assertContextInBody(request);
+ }
+ }
+
+ // ---- streaming synchronizer ----
+
+ @Test
+ public void streamingSynchronizer_defaultUsesGetWithContextInPath() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.all(
+ Handlers.SSE.start(),
+ Handlers.SSE.leaveOpen()))) {
+ Synchronizer synchronizer = new DataSystemComponents.StreamingSynchronizerBuilderImpl()
+ .build(makeInputs(server, false));
+ synchronizer.next();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ synchronizer.close();
+
+ assertEquals("GET", request.getMethod());
+ assertEquals(StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH + contextPathSegment(),
+ request.getPath());
+ assertNoBody(request);
+ }
+ }
+
+ @Test
+ public void streamingSynchronizer_usePostSendsContextInBody() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.all(
+ Handlers.SSE.start(),
+ Handlers.SSE.leaveOpen()))) {
+ Synchronizer synchronizer = new DataSystemComponents.StreamingSynchronizerBuilderImpl()
+ .build(makeInputs(server, true));
+ synchronizer.next();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ synchronizer.close();
+
+ assertEquals("POST", request.getMethod());
+ assertEquals(StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH, request.getPath());
+ assertContextInBody(request);
+ }
+ }
+
+ // ---- FDv1 fallback synchronizer ----
+
+ @Test
+ public void fdv1FallbackSynchronizer_defaultUsesGetWithContextInPath() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson("{}"))) {
+ Synchronizer synchronizer = new DataSystemComponents.FDv1PollingSynchronizerBuilderImpl()
+ .build(makeInputs(server, false));
+ synchronizer.next().get(5, TimeUnit.SECONDS);
+ synchronizer.close();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ assertEquals("GET", request.getMethod());
+ assertEquals(StandardEndpoints.POLLING_REQUEST_GET_BASE_PATH + contextPathSegment(),
+ request.getPath());
+ assertNoBody(request);
+ }
+ }
+
+ @Test
+ public void fdv1FallbackSynchronizer_usePostSendsContextInBodyWithReport() throws Exception {
+ // The FDv1 endpoints do not accept POST; REPORT is their body-carrying method.
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson("{}"))) {
+ Synchronizer synchronizer = new DataSystemComponents.FDv1PollingSynchronizerBuilderImpl()
+ .build(makeInputs(server, true));
+ synchronizer.next().get(5, TimeUnit.SECONDS);
+ synchronizer.close();
+
+ RequestInfo request = server.getRecorder().requireRequest();
+ assertEquals("REPORT", request.getMethod());
+ assertEquals(StandardEndpoints.POLLING_REQUEST_REPORT_BASE_PATH, request.getPath());
+ assertContextInBody(request);
+ }
+ }
+}
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DefaultFDv2RequestorTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DefaultFDv2RequestorTest.java
index a724ea87..574a6c67 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DefaultFDv2RequestorTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DefaultFDv2RequestorTest.java
@@ -36,8 +36,7 @@ public class DefaultFDv2RequestorTest {
private static final LDContext CONTEXT = LDContext.create("test-context-key");
// Bogus paths used by tests — deliberately not the real production paths.
- private static final String GET_REQUEST_PATH = "/fake-poll-get";
- private static final String REPORT_REQUEST_PATH = "/fake-poll-report";
+ private static final String REQUEST_PATH = "/fake-poll";
private static final LDLogger LOGGER = LDLogger.none();
private static HttpProperties httpProperties() {
@@ -49,14 +48,13 @@ private static HttpProperties httpProperties() {
null, null);
}
- private static DefaultFDv2Requestor makeRequestor(HttpServer server, boolean useReport, boolean evaluationReasons, String payloadFilter) {
+ private static DefaultFDv2Requestor makeRequestor(HttpServer server, boolean usePost, boolean evaluationReasons, String payloadFilter) {
return new DefaultFDv2Requestor(
CONTEXT,
server.getUri(),
- GET_REQUEST_PATH,
- REPORT_REQUEST_PATH,
+ REQUEST_PATH,
httpProperties(),
- useReport,
+ usePost,
evaluationReasons,
payloadFilter,
LOGGER);
@@ -93,7 +91,7 @@ public void successfulRequestWithEvents() throws Exception {
assertEquals("payload-transferred", response.getEvents().get(2).getEventType());
RequestInfo req = server.getRecorder().requireRequest();
- assertTrue("path should start with " + GET_REQUEST_PATH, req.getPath().startsWith(GET_REQUEST_PATH));
+ assertTrue("path should start with " + REQUEST_PATH, req.getPath().startsWith(REQUEST_PATH));
}
}
}
@@ -121,7 +119,7 @@ public void requestWithBasisQueryParameter() throws Exception {
future.get(5, TimeUnit.SECONDS);
RequestInfo req = server.getRecorder().requireRequest();
- assertTrue(req.getPath().startsWith(GET_REQUEST_PATH));
+ assertTrue(req.getPath().startsWith(REQUEST_PATH));
assertTrue("query should contain basis=test-state", req.getQuery() != null && req.getQuery().contains("basis=test-state"));
}
}
@@ -260,7 +258,7 @@ public void requestWithBasisContainingState() throws Exception {
future.get(5, TimeUnit.SECONDS);
RequestInfo req = server.getRecorder().requireRequest();
- assertTrue(req.getPath().startsWith(GET_REQUEST_PATH));
+ assertTrue(req.getPath().startsWith(REQUEST_PATH));
assertTrue("query should contain URL-encoded basis", req.getQuery() != null && req.getQuery().contains("basis=%28p%3Apayload-1%3A100%29"));
}
}
@@ -274,7 +272,7 @@ public void requestWithComplexBasisState() throws Exception {
future.get(5, TimeUnit.SECONDS);
RequestInfo req = server.getRecorder().requireRequest();
- assertTrue(req.getPath().startsWith(GET_REQUEST_PATH));
+ assertTrue(req.getPath().startsWith(REQUEST_PATH));
assertTrue("query should contain URL-encoded basis", req.getQuery() != null && req.getQuery().contains("basis=%28p%3Amy-payload%3A200%29"));
}
}
@@ -357,8 +355,7 @@ public void baseUriCanHaveContextPath() throws Exception {
try (DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
CONTEXT,
uri,
- GET_REQUEST_PATH,
- REPORT_REQUEST_PATH,
+ REQUEST_PATH,
httpProperties(),
false,
false,
@@ -368,7 +365,7 @@ public void baseUriCanHaveContextPath() throws Exception {
future.get(5, TimeUnit.SECONDS);
RequestInfo req = server.getRecorder().requireRequest();
- assertTrue("path should start with context path + request path", req.getPath().startsWith("/fake-base" + GET_REQUEST_PATH));
+ assertTrue("path should start with context path + request path", req.getPath().startsWith("/fake-base" + REQUEST_PATH));
}
}
}
@@ -506,18 +503,16 @@ public void evaluationReasonsAddedToRequest() throws Exception {
}
@Test
- public void useReportMethod() throws Exception {
+ public void usePostMethod() throws Exception {
try (HttpServer server = HttpServer.start(Handlers.bodyJson(EMPTY_EVENTS_JSON))) {
try (DefaultFDv2Requestor requestor = makeRequestor(server, true, false, null)) {
requestor.poll(Selector.EMPTY).get(5, TimeUnit.SECONDS);
RequestInfo req = server.getRecorder().requireRequest();
- assertEquals("REPORT", req.getMethod());
- // Context should be in the request body, not the URL path
- assertTrue("path should use REPORT path without context segment",
- req.getPath().startsWith(REPORT_REQUEST_PATH));
- assertFalse("path should not contain GET context segment",
- req.getPath().startsWith(GET_REQUEST_PATH));
+ assertEquals("POST", req.getMethod());
+ // The context goes in the request body, not in the URL path
+ assertEquals("path should be the request path with no context segment",
+ REQUEST_PATH, req.getPath());
assertNotNull("body should contain serialized context", req.getBody());
assertTrue("body should contain context key",
req.getBody().contains("test-context-key"));
@@ -526,23 +521,38 @@ public void useReportMethod() throws Exception {
}
@Test
- public void useReportMethodDoesNotUseEtag() throws Exception {
- // ETag caching is disabled for REPORT requests since the context is in the body
+ public void getMethodAppendsContextToPath() throws Exception {
+ try (HttpServer server = HttpServer.start(Handlers.bodyJson(EMPTY_EVENTS_JSON))) {
+ try (DefaultFDv2Requestor requestor = makeRequestor(server)) {
+ requestor.poll(Selector.EMPTY).get(5, TimeUnit.SECONDS);
+
+ RequestInfo req = server.getRecorder().requireRequest();
+ assertEquals("GET", req.getMethod());
+ assertEquals(REQUEST_PATH + "/" + LDUtil.urlSafeBase64(CONTEXT), req.getPath());
+ assertTrue("GET request should have no body",
+ req.getBody() == null || req.getBody().isEmpty());
+ }
+ }
+ }
+
+ @Test
+ public void usePostMethodDoesNotUseEtag() throws Exception {
+ // ETag caching is disabled for POST requests since the context is in the body
Handler etagResp = Handlers.all(
Handlers.header("ETag", "some-etag"),
Handlers.bodyJson(EMPTY_EVENTS_JSON));
try (HttpServer server = HttpServer.start(etagResp)) {
try (DefaultFDv2Requestor requestor = makeRequestor(server, true, false, null)) {
- // First REPORT request — server returns an ETag, but it should not be cached
+ // First POST request: the server returns an ETag, but it is not stored
requestor.poll(Selector.EMPTY).get(5, TimeUnit.SECONDS);
RequestInfo req1 = server.getRecorder().requireRequest();
- assertNull("REPORT request should not send If-None-Match", req1.getHeader("If-None-Match"));
+ assertNull("POST request should not send If-None-Match", req1.getHeader("If-None-Match"));
- // Second REPORT request — still no If-None-Match because ETag was never stored
+ // Second POST request: still no If-None-Match because the ETag was never stored
requestor.poll(Selector.EMPTY).get(5, TimeUnit.SECONDS);
RequestInfo req2 = server.getRecorder().requireRequest();
- assertNull("REPORT request should never send If-None-Match", req2.getHeader("If-None-Match"));
+ assertNull("POST request should never send If-None-Match", req2.getHeader("If-None-Match"));
}
}
}
@@ -551,7 +561,7 @@ public void useReportMethodDoesNotUseEtag() throws Exception {
public void http400LogsProguardWarning() throws Exception {
try (HttpServer server = HttpServer.start(Handlers.status(400))) {
try (DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
- CONTEXT, server.getUri(), GET_REQUEST_PATH, REPORT_REQUEST_PATH,
+ CONTEXT, server.getUri(), REQUEST_PATH,
httpProperties(), false, false, null, logging.logger)) {
Future future = requestor.poll(Selector.EMPTY);
@@ -569,7 +579,7 @@ public void http400LogsProguardWarning() throws Exception {
public void networkFailureThrowsException() throws Exception {
try (DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
CONTEXT, URI.create("http://localhost:1"),
- GET_REQUEST_PATH, REPORT_REQUEST_PATH,
+ REQUEST_PATH,
httpProperties(), false, false, null, LOGGER)) {
requestor.poll(Selector.EMPTY).get(5, TimeUnit.SECONDS);
}
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilderTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilderTest.java
index a2044ab7..936190f1 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilderTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilderTest.java
@@ -15,6 +15,7 @@
import com.launchdarkly.sdk.android.subsystems.ClientContext;
import com.launchdarkly.sdk.android.subsystems.DataSource;
import com.launchdarkly.sdk.android.subsystems.DataSourceBuilder;
+import com.launchdarkly.sdk.android.subsystems.FDv2SourceResult;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.android.subsystems.Initializer;
import com.launchdarkly.sdk.android.subsystems.Synchronizer;
@@ -25,7 +26,9 @@
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
public class FDv2DataSourceBuilderTest {
@@ -36,9 +39,13 @@ public class FDv2DataSourceBuilderTest {
public LogCaptureRule logging = new LogCaptureRule();
private ClientContext makeClientContext() {
+ return makeClientContext(false);
+ }
+
+ private ClientContext makeClientContext(boolean useReport) {
LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).build();
MockComponents.MockDataSourceUpdateSink sink = new MockComponents.MockDataSourceUpdateSink();
- HttpConfiguration http = new HttpConfiguration(10_000, Collections.emptyMap(), null, false);
+ HttpConfiguration http = new HttpConfiguration(10_000, Collections.emptyMap(), null, useReport);
ClientContext base = new ClientContext(
"mobile-key",
ENV_REPORTER,
@@ -65,6 +72,92 @@ public void defaultBuilder_buildsFDv2DataSource() {
assertTrue(ds instanceof FDv2DataSource);
}
+ @Test
+ public void usePost_isPassedToDataSourceBuilders() throws Exception {
+ AtomicReference seenUsePost = new AtomicReference<>();
+ Map customTable = new LinkedHashMap<>();
+ customTable.put(ConnectionMode.POLLING, new ModeDefinition(
+ Collections.>singletonList(inputs -> {
+ seenUsePost.set(inputs.isUsePost());
+ return new Initializer() {
+ @Override
+ public Future run() {
+ LDAwaitFuture future = new LDAwaitFuture<>();
+ future.set(FDv2SourceResult.status(FDv2SourceResult.Status.shutdown(), false));
+ return future;
+ }
+
+ @Override
+ public void close() {
+ }
+ };
+ }),
+ Collections.>emptyList(),
+ null
+ ));
+
+ FDv2DataSourceBuilder builder = new FDv2DataSourceBuilder(
+ customTable, ConnectionMode.POLLING, ModeResolutionTable.MOBILE, true);
+ assertTrue(builder.isUsePost());
+ DataSource ds = builder.build(makeClientContext());
+
+ // Factories are lazy: start the source so the initializer builder receives the inputs.
+ AwaitableCallback startCallback = new AwaitableCallback<>();
+ ds.start(startCallback);
+ try {
+ startCallback.await(5000);
+ } catch (Exception ignored) {
+ // The start outcome is not under test; only the inputs the builder received.
+ }
+ AwaitableCallback stopCallback = new AwaitableCallback<>();
+ ds.stop(stopCallback);
+ stopCallback.await(2000);
+ builder.close();
+
+ assertEquals(Boolean.TRUE, seenUsePost.get());
+ }
+
+ @Test
+ public void usePost_defaultsToFalse() {
+ assertFalse(new FDv2DataSourceBuilder().isUsePost());
+ Map customTable = new LinkedHashMap<>();
+ customTable.put(ConnectionMode.POLLING, new ModeDefinition(
+ Collections.>emptyList(),
+ Collections.>emptyList(),
+ null
+ ));
+ assertFalse(new FDv2DataSourceBuilder(customTable, ConnectionMode.POLLING).isUsePost());
+ }
+
+ @Test
+ public void useReport_logsWarningOnceAcrossBuilds() {
+ FDv2DataSourceBuilder builder = new FDv2DataSourceBuilder();
+ ClientContext ctx = makeClientContext(true);
+ builder.build(ctx);
+ builder.build(ctx);
+ builder.close();
+
+ int warnings = 0;
+ for (String message : logging.logCapture.getMessageStrings()) {
+ if (message.contains("WARN:") && message.contains("useReport")) {
+ warnings++;
+ }
+ }
+ assertEquals(1, warnings);
+ logging.assertWarnLogged("DataSystemBuilder.usePost");
+ }
+
+ @Test
+ public void useReportDisabled_logsNoUseReportWarning() {
+ FDv2DataSourceBuilder builder = new FDv2DataSourceBuilder();
+ builder.build(makeClientContext(false));
+ builder.close();
+
+ for (String message : logging.logCapture.getMessageStrings()) {
+ assertFalse(message, message.contains("useReport"));
+ }
+ }
+
@Test
public void customModeTable_buildsCorrectly() {
Map customTable = new LinkedHashMap<>();
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java
index 7065d6d7..1f58a044 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java
@@ -37,7 +37,7 @@ public class FDv2EntryConverterTest {
private static DataSourceBuildInputs plainInputs() {
return new DataSourceBuildInputs(
- CONTEXT, null, null, false,
+ CONTEXT, null, null, false, false,
() -> Selector.EMPTY, null, CACHE_DIR,
LDLogger.none());
}
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizerTest.java
index 9aa3891d..7589b5d7 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizerTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2StreamingSynchronizerTest.java
@@ -51,8 +51,7 @@ public void tearDown() {
// Bogus paths used by tests — deliberately not the real production paths.
private static final String STREAM_PATH = "/fake-stream";
- private static final String POLL_GET_PATH = "/fake-poll-get";
- private static final String POLL_REPORT_PATH = "/fake-poll-report";
+ private static final String POLL_PATH = "/fake-poll";
private static HttpProperties httpProperties() {
return new HttpProperties(
@@ -92,10 +91,10 @@ private FDv2StreamingSynchronizer makeSynchronizer(
private FDv2StreamingSynchronizer makeSynchronizer(
URI streamBaseUri,
- boolean evaluationReasons, boolean useReport) {
+ boolean evaluationReasons, boolean usePost) {
return new FDv2StreamingSynchronizer(
CONTEXT, EMPTY_SELECTOR_SOURCE, streamBaseUri, STREAM_PATH,
- null, 100, evaluationReasons, useReport,
+ null, 100, evaluationReasons, usePost,
httpProperties(), executor, LOGGER, null);
}
@@ -1051,7 +1050,7 @@ public void evaluationReasonsAddedToStreamUrl() throws Exception {
}
@Test
- public void useReportMethodForStreamRequest() throws Exception {
+ public void usePostMethodForStreamRequest() throws Exception {
String serverIntent = makeEvent("server-intent", "{\"payloads\":[{\"id\":\"payload-1\",\"target\":100,\"intentCode\":\"xfer-full\",\"reason\":\"payload-missing\"}]}");
String payloadTransferred = makeEvent("payload-transferred", "{\"state\":\"(p:payload-1:100)\",\"version\":100}");
@@ -1062,17 +1061,17 @@ public void useReportMethodForStreamRequest() throws Exception {
Handlers.SSE.leaveOpen()))) {
FDv2StreamingSynchronizer sync = makeSynchronizer(server.getUri(),
- false, true /* useReport */);
+ false, true /* usePost */);
Future resultFuture = sync.next();
FDv2SourceResult result = resultFuture.get(5, TimeUnit.SECONDS);
assertEquals(SourceResultType.CHANGE_SET, result.getResultType());
RequestInfo request = server.getRecorder().requireRequest();
- assertEquals("REPORT", request.getMethod());
- // With REPORT the context goes in the body, not the URL path
- assertFalse("path should not contain base64 context segment when using REPORT",
- request.getPath().contains(LDUtil.urlSafeBase64(CONTEXT)));
+ assertEquals("POST", request.getMethod());
+ // With POST the context goes in the body, not the URL path
+ assertEquals("path should be the stream path with no context segment",
+ STREAM_PATH, request.getPath());
assertNotNull("body should contain serialized context", request.getBody());
assertTrue("body should contain context key",
request.getBody().contains("test-context"));
@@ -1126,7 +1125,7 @@ public void pingEventWithRequestorTriggersPoll() throws Exception {
DefaultFDv2Requestor requestor = new DefaultFDv2Requestor(
CONTEXT, pollServer.getUri(),
- POLL_GET_PATH, POLL_REPORT_PATH,
+ POLL_PATH,
httpProperties(), false, false, null, LOGGER);
FDv2StreamingSynchronizer sync = new FDv2StreamingSynchronizer(
diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigDataSystemTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigDataSystemTest.java
index a29aacba..ee39916d 100644
--- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigDataSystemTest.java
+++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDConfigDataSystemTest.java
@@ -54,6 +54,21 @@ public void dataSystem_resolutionTable_matchesCreateMobileForForegroundAndBackgr
assertResolutionTablesAgree(expected, fdv2.getResolutionTable());
}
+ @Test
+ public void dataSystem_propagatesUsePostToFDv2DataSourceBuilder() {
+ LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled)
+ .mobileKey("k")
+ .dataSystem(Components.dataSystem().usePost(true))
+ .build();
+ assertTrue(((FDv2DataSourceBuilder) config.dataSource).isUsePost());
+
+ LDConfig defaultConfig = new LDConfig.Builder(AutoEnvAttributes.Disabled)
+ .mobileKey("k")
+ .dataSystem(Components.dataSystem())
+ .build();
+ assertFalse(((FDv2DataSourceBuilder) defaultConfig.dataSource).isUsePost());
+ }
+
@Test
public void dataSystem_propagatesAutomaticModeSwitchingFromDataSystemBuilder() {
AutomaticModeSwitchingConfig granular = DataSystemComponents.automaticModeSwitching()
diff --git a/testharness-suppressions-fdv2.txt b/testharness-suppressions-fdv2.txt
index 5e53af75..ceb41bc5 100644
--- a/testharness-suppressions-fdv2.txt
+++ b/testharness-suppressions-fdv2.txt
@@ -1 +1,4 @@
-streaming/fdv2/fallback to FDv1 handling
\ No newline at end of file
+streaming/fdv2/fallback to FDv1 handling
+events/feature events/single-kind anonymous context redacts all attributes
+events/feature events/multi-kind with anonymous context redacts attributes appropriately
+events/context properties/single-kind, allAttributesPrivate, slash-prefixed attribute name