Skip to content

Commit 2cee183

Browse files
committed
Harden agentless configuration responses
Rely on OkHttp gzip negotiation and cover truncated response cache preservation. Require JSON:API envelopes for custom endpoints.
1 parent 76917d6 commit 2cee183

3 files changed

Lines changed: 78 additions & 9 deletions

File tree

products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/AgentlessConfigurationSource.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,7 @@ private boolean apply(final UfcHttpResponse response) {
255255
try {
256256
configuration =
257257
RemoteConfigServiceImpl.UniversalFlagConfigDeserializer.INSTANCE.deserializeApiResponse(
258-
response.body,
259-
config.getFeatureFlaggingConfigurationSourceAgentlessBaseUrl() != null);
258+
response.body);
260259
} catch (final IOException | RuntimeException e) {
261260
LOGGER.debug("Feature Flagging HTTP configuration source returned malformed UFC payload", e);
262261
return false;
@@ -373,6 +372,7 @@ public UfcHttpResponse fetch(final HttpUrl endpoint, final Config config, final
373372
if (etag != null) {
374373
headers.put("If-None-Match", etag);
375374
}
375+
// Leave Accept-Encoding unset so OkHttp negotiates gzip and transparently decompresses it.
376376
final Request request = prepareRequest(endpoint, headers, config, true).get().build();
377377
final Call call = httpClient.newCall(request);
378378
if (!activeCall.compareAndSet(null, call)) {

products/feature-flagging/feature-flagging-lib/src/main/java/com/datadog/featureflag/RemoteConfigServiceImpl.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ public ServerConfiguration deserialize(final byte[] content) throws IOException
8484
}
8585

8686
@Nullable
87-
ServerConfiguration deserializeApiResponse(
88-
final byte[] content, final boolean allowRawConfiguration) throws IOException {
87+
ServerConfiguration deserializeApiResponse(final byte[] content) throws IOException {
8988
final Map<String, Object> response =
9089
API_RESPONSE_ADAPTER.fromJson(
9190
Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
@@ -99,7 +98,7 @@ ServerConfiguration deserializeApiResponse(
9998
? validConfiguration(V1_ADAPTER.fromJsonValue(dataAttributes.get("attributes")))
10099
: null;
101100
}
102-
return allowRawConfiguration ? validConfiguration(deserialize(content)) : null;
101+
return null;
103102
}
104103

105104
@Nullable

products/feature-flagging/feature-flagging-lib/src/test/java/com/datadog/featureflag/AgentlessConfigurationSourceTest.java

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import datadog.trace.api.Config;
2222
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
2323
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
24+
import java.io.ByteArrayOutputStream;
2425
import java.io.IOException;
2526
import java.net.HttpURLConnection;
2627
import java.net.SocketTimeoutException;
2728
import java.util.ArrayList;
29+
import java.util.Arrays;
2830
import java.util.List;
2931
import java.util.concurrent.BlockingQueue;
3032
import java.util.concurrent.CountDownLatch;
@@ -36,6 +38,7 @@
3638
import java.util.concurrent.ScheduledExecutorService;
3739
import java.util.concurrent.TimeUnit;
3840
import java.util.concurrent.atomic.AtomicInteger;
41+
import java.util.zip.GZIPOutputStream;
3942
import okhttp3.Call;
4043
import okhttp3.HttpUrl;
4144
import okhttp3.OkHttpClient;
@@ -157,13 +160,67 @@ void realHttpClientSendsAgentlessHeadersAndReadsResponse() throws Exception {
157160
assertEquals("test-api-key", server.getLastRequest().getHeader("DD-API-KEY"));
158161
assertEquals("etag-a", server.getLastRequest().getHeader("If-None-Match"));
159162
assertEquals("java", server.getLastRequest().getHeader("Datadog-Meta-Lang"));
163+
assertEquals("gzip", server.getLastRequest().getHeader("Accept-Encoding"));
160164
} finally {
161165
httpClient.dispatcher().executorService().shutdownNow();
162166
httpClient.connectionPool().evictAll();
163167
}
164168
}
165169
}
166170

171+
@Test
172+
void handlesGzipAndKeepsLastKnownGoodWhenNextResponseIsTruncated() throws Exception {
173+
final byte[] compressedConfig = gzip(emptyConfig());
174+
final byte[] truncatedConfig = Arrays.copyOf(compressedConfig, compressedConfig.length - 8);
175+
final AtomicInteger responses = new AtomicInteger();
176+
try (JavaTestHttpServer server =
177+
JavaTestHttpServer.httpServer(
178+
s ->
179+
s.handlers(
180+
h ->
181+
h.get(
182+
CONFIG_PATH,
183+
api -> {
184+
final boolean firstResponse = responses.getAndIncrement() == 0;
185+
final byte[] body =
186+
firstResponse ? compressedConfig : truncatedConfig;
187+
api.getResponse()
188+
.addHeader("Content-Encoding", "gzip")
189+
.addHeader("ETag", firstResponse ? "etag-good" : "etag-bad")
190+
.sendWithType("application/json", body);
191+
})))) {
192+
final HttpUrl endpoint = HttpUrl.get(server.getAddress().resolve(CONFIG_PATH));
193+
final OkHttpClient httpClient = new OkHttpClient.Builder().build();
194+
final AgentlessConfigurationSource service =
195+
new AgentlessConfigurationSource(
196+
endpoint,
197+
config(),
198+
30_000,
199+
new AgentlessConfigurationSource.OkHttpUfcHttpClient(httpClient),
200+
Executors.newSingleThreadScheduledExecutor(),
201+
delay -> {},
202+
() -> 1.0);
203+
final ArgumentCaptor<ServerConfiguration> configuration =
204+
ArgumentCaptor.forClass(ServerConfiguration.class);
205+
FeatureFlaggingGateway.addConfigListener(listener);
206+
207+
try {
208+
assertTrue(service.pollOnce());
209+
assertFalse(service.pollOnce());
210+
211+
verify(listener).accept(configuration.capture());
212+
assertEquals("Staging", configuration.getValue().environment.name);
213+
assertEquals(4, responses.get());
214+
assertEquals("gzip", server.getLastRequest().getHeader("Accept-Encoding"));
215+
assertEquals("etag-good", server.getLastRequest().getHeader("If-None-Match"));
216+
} finally {
217+
service.close();
218+
httpClient.dispatcher().executorService().shutdownNow();
219+
httpClient.connectionPool().evictAll();
220+
}
221+
}
222+
}
223+
167224
@Test
168225
void downloadsAndAppliesLargeUfcWithoutPayloadLimit() throws Exception {
169226
final int flagCount = 5_000;
@@ -378,22 +435,27 @@ void appliesAcceptedJsonApiUfcThroughGatewayAndSendsApiKey() throws Exception {
378435
}
379436

380437
@Test
381-
void appliesRawUfcFromConfiguredCompatibilityEndpoint() throws Exception {
382-
final FakeClient client = new FakeClient(response(200, "etag-a", emptyConfigAttributes()));
438+
void customEndpointRequiresJsonApiAndKeepsLastKnownGoodOnRawUfc() throws Exception {
439+
final FakeClient client =
440+
new FakeClient(
441+
response(200, "etag-good", emptyConfig()),
442+
response(200, "etag-raw", emptyConfigAttributes()));
383443
final Config config = config();
384444
lenient()
385445
.when(config.getFeatureFlaggingConfigurationSourceAgentlessBaseUrl())
386-
.thenReturn("http://compatibility-backend/custom/ufc");
446+
.thenReturn("http://custom-backend/custom/ufc");
387447
final AgentlessConfigurationSource service = service(client, config);
388448
final ArgumentCaptor<ServerConfiguration> configuration =
389449
ArgumentCaptor.forClass(ServerConfiguration.class);
390450
FeatureFlaggingGateway.addConfigListener(listener);
391451

392452
assertTrue(service.pollOnce());
453+
assertFalse(service.pollOnce());
393454

394455
verify(listener).accept(configuration.capture());
395456
assertEquals("Staging", configuration.getValue().environment.name);
396-
assertTrue(configuration.getValue().flags.isEmpty());
457+
assertNull(client.requests.get(0).etag);
458+
assertEquals("etag-good", client.requests.get(1).etag);
397459
}
398460

399461
@Test
@@ -1032,6 +1094,14 @@ private static String largeConfig(final int flagCount) {
10321094
return jsonApiResponse("universal-flag-configuration", json.append("}}").toString());
10331095
}
10341096

1097+
private static byte[] gzip(final String value) throws IOException {
1098+
final ByteArrayOutputStream output = new ByteArrayOutputStream();
1099+
try (GZIPOutputStream gzip = new GZIPOutputStream(output)) {
1100+
gzip.write(value.getBytes(UTF_8));
1101+
}
1102+
return output.toByteArray();
1103+
}
1104+
10351105
private static void awaitCalls(final FakeClient client, final int count) throws Exception {
10361106
for (int i = 0; i < 100; i++) {
10371107
if (client.calls.get() >= count) {

0 commit comments

Comments
 (0)