Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static class SdkConfigClientSideParams {
LDContext initialContext;
boolean evaluationReasons;
boolean useReport;
boolean usePost;
boolean includeEnvironmentAttributes;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,

builder.evaluationReasons(params.clientSide.evaluationReasons);
builder.http(
Components.httpConfiguration().useReport(params.clientSide.useReport)
Components.httpConfiguration()
.useReport(params.clientSide.useReport)
.usePost(params.clientSide.usePost)
);

if (params.tags != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public class TestService extends NanoHTTPD {
"client-per-context-summaries",
"client-event-source-http-errors",
"retry-conformance-fdv1-streaming",
"retry-conformance-fdv1-polling"
"retry-conformance-fdv1-polling",
"client-use-post"
};
private static final String MIME_JSON = "application/json";
static final Gson gson = new GsonBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,8 @@ public HttpConfiguration build(ClientContext clientContext) {
connectTimeoutMillis,
headers,
headerTransform,
useReport
useReport,
usePost
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.getHttp().shouldUsePost(),
httpProps, inputs.getSharedExecutor(),
inputs.getBaseLogger(), null);
}
Expand All @@ -159,10 +159,13 @@ public FDv1PollingSynchronizerBuilderImpl pollIntervalMillis(int pollIntervalMil

@Override
public Synchronizer build(DataSourceBuildInputs inputs) {
// The FDv1 endpoints have no POST variant. When usePost is enabled, the fetcher
// sends the context in the request body with REPORT so it stays out of the
// request URI. useReport has no effect inside the data system.
FeatureFetcher fetcher = new HttpFeatureFlagFetcher(
inputs.getServiceEndpoints().getPollingBaseUri(),
inputs.isEvaluationReasons(),
inputs.getHttp().isUseReport(),
inputs.getHttp().shouldUsePost(),
LDUtil.makeHttpProperties(inputs.getHttp()),
inputs.getCacheDir(),
inputs.getBaseLogger()
Expand Down Expand Up @@ -361,8 +364,8 @@ private static FDv2Requestor makePollingRequestor(DataSourceBuildInputs inputs,
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_POST_BASE_PATH,
httpProps, inputs.getHttp().shouldUsePost(),
inputs.isEvaluationReasons(), null, inputs.getBaseLogger());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@
/**
* Default OkHttp-based implementation of {@link FDv2Requestor}.
* <p>
* 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
* needed or used.
* needed or used. ETag tracking runs only for GET requests.
* <p>
* 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";
Expand All @@ -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;
Expand All @@ -68,9 +67,9 @@ 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 postRequestPath path for POST requests (context sent in the request body)
* @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 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
Expand All @@ -80,28 +79,28 @@ final class DefaultFDv2Requestor implements FDv2Requestor {
@NonNull LDContext evaluationContext,
@NonNull URI baseUri,
@NonNull String getRequestPath,
@NonNull String reportRequestPath,
@NonNull String postRequestPath,
@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;
this.headers = httpProperties.toHeadersBuilder().build();

// 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
usePost ? postRequestPath : getRequestPath);
this.pollingUri = usePost
? basePollingUri
: HttpHelpers.concatenateUriPath(basePollingUri, LDUtil.urlSafeBase64(evaluationContext));
this.reportBody = useReport
this.postBody = usePost
? RequestBody.create(JsonSerialization.serialize(evaluationContext), JSON)
: null;

Expand Down Expand Up @@ -139,8 +138,8 @@ public Future<FDv2PayloadResponse> 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)) {
Expand Down Expand Up @@ -207,7 +206,7 @@ private void handleResponse(
return;
}

if (!useReport) {
if (!usePost) {
synchronized (etagLock) {
cachedEtag = response.header(ETAG_HEADER);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class FDv2DataSourceBuilder implements ComponentConfigurer<DataSource>, Closeabl
private boolean includeInitializers = true; // start with initializers
private ScheduledExecutorService sharedExecutor;

// The useReport warning logs once per builder; build runs again on every
// connection mode switch.
private boolean useReportWarningLogged;

FDv2DataSourceBuilder() {
this(DataSystemComponents.makeDefaultModeTable(), ConnectionMode.STREAMING, ModeResolutionTable.MOBILE);
}
Expand Down Expand Up @@ -98,6 +102,13 @@ ModeDefinition getModeDefinition(@NonNull ConnectionMode mode) {

@Override
public DataSource build(ClientContext clientContext) {
if (clientContext.getHttp().isUseReport() && !useReportWarningLogged) {
useReportWarningLogged = true;
clientContext.getBaseLogger().warn(
"The useReport option has no effect when the data system is configured;"
+ " requests use GET unless usePost is enabled.");
}

if (sharedExecutor == null) {
// Pool size 4: supports the FDv2DataSource main loop (1), an active synchronizer
// such as the streaming event loop (1), FDv2DataSource condition timers for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@
* <p>
*/
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

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
Expand Down Expand Up @@ -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 via HTTP 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
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private StandardEndpoints() {}
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";
static final String FDV2_POLLING_REQUEST_POST_BASE_PATH = "/sdk/poll/eval";
static final String FDV2_STREAMING_REQUEST_BASE_PATH = "/sdk/stream/eval";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public abstract class HttpConfigurationBuilder implements ComponentConfigurer<Ht
*/
protected boolean useReport;

/**
* Whether the HTTP POST method is used for FDv2 flag requests
*/
protected boolean usePost;

/**
* Name of the wrapping library
*/
Expand Down Expand Up @@ -101,6 +106,25 @@ public HttpConfigurationBuilder useReport(boolean useReport) {
return this;
}

/**
* Sets whether to use the HTTP POST method for feature flag requests.
* <p>
* By default, polling and streaming connections are made with the GET method, with the
* evaluation context data encoded into the request URI. When this option is enabled, the
* SDK sends the context data in the request body instead. This keeps the context out of
* URL-based request logs and CDN logs.
* <p>
* This option only takes effect when the data system is configured with
* {@link com.launchdarkly.sdk.android.LDConfig.Builder#dataSystem(DataSystemBuilder)}.
*
* @param usePost true to enable the POST method
* @return the builder
*/
public HttpConfigurationBuilder usePost(boolean usePost) {
this.usePost = usePost;
return this;
}

/**
* For use by wrapper libraries to set an identifying name for the wrapper being used. This will be included in a
* header during requests to the LaunchDarkly servers to allow recording metrics on the usage of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public final class HttpConfiguration {
private final Map<String, String> defaultHeaders;
private final LDHeaderUpdater headerTransform;
private final boolean useReport;
private final boolean usePost;

/**
* Creates an instance.
Expand All @@ -33,18 +34,38 @@ public final class HttpConfiguration {
* @param defaultHeaders see {@link #getDefaultHeaders()}
* @param headerTransform see {@link #getHeaderTransform()}
* @param useReport see {@link #isUseReport()}
* @param usePost see {@link #shouldUsePost()}
*/
public HttpConfiguration(
int connectTimeoutMillis,
Map<String, String> defaultHeaders,
LDHeaderUpdater headerTransform,
boolean useReport
boolean useReport,
boolean usePost
) {
super();
this.connectTimeoutMillis = connectTimeoutMillis;
this.defaultHeaders = defaultHeaders == null ? emptyMap() : new HashMap<>(defaultHeaders);
this.headerTransform = headerTransform;
this.useReport = useReport;
this.usePost = usePost;
}

/**
* Creates an instance with {@code usePost} disabled.
*
* @param connectTimeoutMillis see {@link #getConnectTimeoutMillis()}
* @param defaultHeaders see {@link #getDefaultHeaders()}
* @param headerTransform see {@link #getHeaderTransform()}
* @param useReport see {@link #isUseReport()}
*/
public HttpConfiguration(
int connectTimeoutMillis,
Map<String, String> defaultHeaders,
LDHeaderUpdater headerTransform,
boolean useReport
) {
this(connectTimeoutMillis, defaultHeaders, headerTransform, useReport, false);
}

/**
Expand Down Expand Up @@ -84,4 +105,13 @@ public LDHeaderUpdater getHeaderTransform() {
public boolean isUseReport() {
return useReport;
}

/**
* The setting for whether to use the HTTP POST method for FDv2 flag requests.
*
* @return true to use HTTP POST
*/
public boolean shouldUsePost() {
return usePost;
}
}
Loading
Loading