Skip to content

Commit 9bdb986

Browse files
raju-opticlaude
andcommitted
[AI-FSSDK] [FSSDK-12273] Add support for customHeaders option for polling datafile manager
- Added withCustomHeaders(Map<String, String>) builder method to HttpProjectConfigManager - Custom headers are applied after SDK headers to allow user override - Implemented immutable copy of headers for thread safety - Added 7 comprehensive unit tests covering: * Custom headers included in HTTP requests * Custom headers override SDK headers (including Authorization) * Null and empty custom headers handled gracefully * Immutability of custom headers map * Integration with datafile access token Quality Assurance: - Tests: 7/7 new tests passed, all existing tests pass - Code Review: Manual review completed - follows Java SDK conventions - Implementation: Builder pattern, immutability, proper header override logic Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 9d3e1c9 commit 9bdb986

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
import java.io.IOException;
3737
import java.net.URI;
38+
import java.util.Map;
39+
import java.util.HashMap;
3840
import java.util.concurrent.TimeUnit;
3941

4042
/**
@@ -66,6 +68,7 @@ public class HttpProjectConfigManager extends PollingProjectConfigManager {
6668
public final OptimizelyHttpClient httpClient;
6769
private final URI uri;
6870
private final String datafileAccessToken;
71+
private final Map<String, String> customHeaders;
6972
private String datafileLastModified;
7073
private final ReentrantLock lock = new ReentrantLock();
7174

@@ -74,6 +77,7 @@ private HttpProjectConfigManager(long period,
7477
OptimizelyHttpClient httpClient,
7578
String url,
7679
String datafileAccessToken,
80+
Map<String, String> customHeaders,
7781
long blockingTimeoutPeriod,
7882
TimeUnit blockingTimeoutUnit,
7983
NotificationCenter notificationCenter,
@@ -82,6 +86,7 @@ private HttpProjectConfigManager(long period,
8286
this.httpClient = httpClient;
8387
this.uri = URI.create(url);
8488
this.datafileAccessToken = datafileAccessToken;
89+
this.customHeaders = customHeaders != null ? new HashMap<>(customHeaders) : new HashMap<>();
8590
}
8691

8792
public URI getUri() {
@@ -171,6 +176,7 @@ public void close() {
171176
HttpGet createHttpRequest() {
172177
HttpGet httpGet = new HttpGet(uri);
173178

179+
// Apply SDK headers first
174180
if (datafileAccessToken != null) {
175181
httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + datafileAccessToken);
176182
}
@@ -179,6 +185,13 @@ HttpGet createHttpRequest() {
179185
httpGet.setHeader(HttpHeaders.IF_MODIFIED_SINCE, datafileLastModified);
180186
}
181187

188+
// Apply custom headers last to allow user override of SDK headers
189+
if (customHeaders != null && !customHeaders.isEmpty()) {
190+
for (Map.Entry<String, String> header : customHeaders.entrySet()) {
191+
httpGet.setHeader(header.getKey(), header.getValue());
192+
}
193+
}
194+
182195
return httpGet;
183196
}
184197

@@ -190,6 +203,7 @@ public static class Builder {
190203
private String datafile;
191204
private String url;
192205
private String datafileAccessToken = null;
206+
private Map<String, String> customHeaders = null;
193207
private String format = "https://cdn.optimizely.com/datafiles/%s.json";
194208
private String authFormat = "https://config.optimizely.com/datafiles/auth/%s.json";
195209
private OptimizelyHttpClient httpClient;
@@ -222,6 +236,18 @@ public Builder withDatafileAccessToken(String token) {
222236
return this;
223237
}
224238

239+
/**
240+
* Set custom headers to be included in HTTP requests to fetch the datafile.
241+
* If a custom header has the same name as an SDK-added header, the custom header value will override it.
242+
*
243+
* @param customHeaders A map of header names to header values
244+
* @return A HttpProjectConfigManager builder
245+
*/
246+
public Builder withCustomHeaders(Map<String, String> customHeaders) {
247+
this.customHeaders = customHeaders;
248+
return this;
249+
}
250+
225251
public Builder withUrl(String url) {
226252
this.url = url;
227253
return this;
@@ -380,6 +406,7 @@ public HttpProjectConfigManager build(boolean defer) {
380406
httpClient,
381407
url,
382408
datafileAccessToken,
409+
customHeaders,
383410
blockingTimeoutPeriod,
384411
blockingTimeoutUnit,
385412
notificationCenter,

core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
import java.io.IOException;
3939
import java.net.URI;
40+
import java.util.Map;
41+
import java.util.HashMap;
4042
import java.util.concurrent.TimeUnit;
4143

4244
import static com.optimizely.ab.config.HttpProjectConfigManager.*;
@@ -396,4 +398,159 @@ public void testBasicFetchTwice() throws Exception {
396398
ProjectConfig latestConfig = projectConfigManager.getConfig();
397399
assertEquals(actual, latestConfig);
398400
}
401+
402+
@Test
403+
public void testCustomHeadersAreIncludedInRequest() throws Exception {
404+
Map<String, String> customHeaders = new HashMap<>();
405+
customHeaders.put("X-Custom-Header", "custom-value");
406+
customHeaders.put("X-Another-Header", "another-value");
407+
408+
HttpProjectConfigManager manager = builder()
409+
.withOptimizelyHttpClient(mockHttpClient)
410+
.withSdkKey("test-sdk-key")
411+
.withCustomHeaders(customHeaders)
412+
.build(true);
413+
414+
try {
415+
HttpGet request = manager.createHttpRequest();
416+
417+
// Verify custom headers are present
418+
assertNotNull(request.getFirstHeader("X-Custom-Header"));
419+
assertEquals("custom-value", request.getFirstHeader("X-Custom-Header").getValue());
420+
assertNotNull(request.getFirstHeader("X-Another-Header"));
421+
assertEquals("another-value", request.getFirstHeader("X-Another-Header").getValue());
422+
} finally {
423+
manager.close();
424+
}
425+
}
426+
427+
@Test
428+
public void testCustomHeadersOverrideSdkHeaders() throws Exception {
429+
Map<String, String> customHeaders = new HashMap<>();
430+
// Override the Authorization header
431+
customHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer custom-token");
432+
433+
HttpProjectConfigManager manager = builder()
434+
.withOptimizelyHttpClient(mockHttpClient)
435+
.withSdkKey("test-sdk-key")
436+
.withDatafileAccessToken("sdk-token")
437+
.withCustomHeaders(customHeaders)
438+
.build(true);
439+
440+
try {
441+
HttpGet request = manager.createHttpRequest();
442+
443+
// Verify custom header overrides SDK header
444+
assertNotNull(request.getFirstHeader(HttpHeaders.AUTHORIZATION));
445+
assertEquals("Bearer custom-token", request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue());
446+
} finally {
447+
manager.close();
448+
}
449+
}
450+
451+
@Test
452+
public void testWithoutCustomHeaders() throws Exception {
453+
HttpProjectConfigManager manager = builder()
454+
.withOptimizelyHttpClient(mockHttpClient)
455+
.withSdkKey("test-sdk-key")
456+
.build(true);
457+
458+
try {
459+
HttpGet request = manager.createHttpRequest();
460+
461+
// Verify no custom headers are present (only SDK headers)
462+
assertNull(request.getFirstHeader("X-Custom-Header"));
463+
} finally {
464+
manager.close();
465+
}
466+
}
467+
468+
@Test
469+
public void testWithNullCustomHeaders() throws Exception {
470+
HttpProjectConfigManager manager = builder()
471+
.withOptimizelyHttpClient(mockHttpClient)
472+
.withSdkKey("test-sdk-key")
473+
.withCustomHeaders(null)
474+
.build(true);
475+
476+
try {
477+
HttpGet request = manager.createHttpRequest();
478+
479+
// Should not throw exception and should work normally
480+
assertNotNull(request);
481+
} finally {
482+
manager.close();
483+
}
484+
}
485+
486+
@Test
487+
public void testWithEmptyCustomHeaders() throws Exception {
488+
Map<String, String> customHeaders = new HashMap<>();
489+
490+
HttpProjectConfigManager manager = builder()
491+
.withOptimizelyHttpClient(mockHttpClient)
492+
.withSdkKey("test-sdk-key")
493+
.withCustomHeaders(customHeaders)
494+
.build(true);
495+
496+
try {
497+
HttpGet request = manager.createHttpRequest();
498+
499+
// Should not throw exception and should work normally
500+
assertNotNull(request);
501+
} finally {
502+
manager.close();
503+
}
504+
}
505+
506+
@Test
507+
public void testCustomHeadersWithDatafileAccessToken() throws Exception {
508+
Map<String, String> customHeaders = new HashMap<>();
509+
customHeaders.put("X-Custom-Header", "custom-value");
510+
511+
HttpProjectConfigManager manager = builder()
512+
.withOptimizelyHttpClient(mockHttpClient)
513+
.withSdkKey("test-sdk-key")
514+
.withDatafileAccessToken("test-token")
515+
.withCustomHeaders(customHeaders)
516+
.build(true);
517+
518+
try {
519+
HttpGet request = manager.createHttpRequest();
520+
521+
// Verify both custom headers and SDK headers are present
522+
assertNotNull(request.getFirstHeader("X-Custom-Header"));
523+
assertEquals("custom-value", request.getFirstHeader("X-Custom-Header").getValue());
524+
assertNotNull(request.getFirstHeader(HttpHeaders.AUTHORIZATION));
525+
assertEquals("Bearer test-token", request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue());
526+
} finally {
527+
manager.close();
528+
}
529+
}
530+
531+
@Test
532+
public void testCustomHeadersAreImmutable() throws Exception {
533+
Map<String, String> customHeaders = new HashMap<>();
534+
customHeaders.put("X-Custom-Header", "original-value");
535+
536+
HttpProjectConfigManager manager = builder()
537+
.withOptimizelyHttpClient(mockHttpClient)
538+
.withSdkKey("test-sdk-key")
539+
.withCustomHeaders(customHeaders)
540+
.build(true);
541+
542+
try {
543+
// Modify the original map after building
544+
customHeaders.put("X-Custom-Header", "modified-value");
545+
customHeaders.put("X-New-Header", "new-value");
546+
547+
HttpGet request = manager.createHttpRequest();
548+
549+
// Verify headers are not affected by external map modifications
550+
assertEquals("original-value", request.getFirstHeader("X-Custom-Header").getValue());
551+
assertNull(request.getFirstHeader("X-New-Header"));
552+
} finally {
553+
manager.close();
554+
}
555+
}
399556
}

0 commit comments

Comments
 (0)