Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
Expand Down Expand Up @@ -66,6 +68,7 @@ public class HttpProjectConfigManager extends PollingProjectConfigManager {
public final OptimizelyHttpClient httpClient;
private final URI uri;
private final String datafileAccessToken;
private final Map<String, String> customHeaders;
private String datafileLastModified;
private final ReentrantLock lock = new ReentrantLock();

Expand All @@ -74,6 +77,7 @@ private HttpProjectConfigManager(long period,
OptimizelyHttpClient httpClient,
String url,
String datafileAccessToken,
Map<String, String> customHeaders,
long blockingTimeoutPeriod,
TimeUnit blockingTimeoutUnit,
NotificationCenter notificationCenter,
Expand All @@ -82,6 +86,7 @@ private HttpProjectConfigManager(long period,
this.httpClient = httpClient;
this.uri = URI.create(url);
this.datafileAccessToken = datafileAccessToken;
this.customHeaders = customHeaders != null ? new HashMap<>(customHeaders) : new HashMap<>();
}

public URI getUri() {
Expand Down Expand Up @@ -171,6 +176,7 @@ public void close() {
HttpGet createHttpRequest() {
HttpGet httpGet = new HttpGet(uri);

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

// Apply custom headers last to allow user override of SDK headers
if (customHeaders != null && !customHeaders.isEmpty()) {
for (Map.Entry<String, String> header : customHeaders.entrySet()) {
httpGet.setHeader(header.getKey(), header.getValue());
}
}

return httpGet;
}

Expand All @@ -190,6 +203,7 @@ public static class Builder {
private String datafile;
private String url;
private String datafileAccessToken = null;
private Map<String, String> customHeaders = null;
private String format = "https://cdn.optimizely.com/datafiles/%s.json";
private String authFormat = "https://config.optimizely.com/datafiles/auth/%s.json";
private OptimizelyHttpClient httpClient;
Expand Down Expand Up @@ -222,6 +236,18 @@ public Builder withDatafileAccessToken(String token) {
return this;
}

/**
* Set custom headers to be included in HTTP requests to fetch the datafile.
* If a custom header has the same name as an SDK-added header, the custom header value will override it.
*
* @param customHeaders A map of header names to header values
* @return A HttpProjectConfigManager builder
*/
public Builder withCustomHeaders(Map<String, String> customHeaders) {
this.customHeaders = customHeaders;
return this;
}

public Builder withUrl(String url) {
this.url = url;
return this;
Expand Down Expand Up @@ -380,6 +406,7 @@ public HttpProjectConfigManager build(boolean defer) {
httpClient,
url,
datafileAccessToken,
customHeaders,
blockingTimeoutPeriod,
blockingTimeoutUnit,
notificationCenter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

import static com.optimizely.ab.config.HttpProjectConfigManager.*;
Expand Down Expand Up @@ -396,4 +398,159 @@ public void testBasicFetchTwice() throws Exception {
ProjectConfig latestConfig = projectConfigManager.getConfig();
assertEquals(actual, latestConfig);
}

@Test
public void testCustomHeadersAreIncludedInRequest() throws Exception {
Map<String, String> customHeaders = new HashMap<>();
customHeaders.put("X-Custom-Header", "custom-value");
customHeaders.put("X-Another-Header", "another-value");

HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withCustomHeaders(customHeaders)
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Verify custom headers are present
assertNotNull(request.getFirstHeader("X-Custom-Header"));
assertEquals("custom-value", request.getFirstHeader("X-Custom-Header").getValue());
assertNotNull(request.getFirstHeader("X-Another-Header"));
assertEquals("another-value", request.getFirstHeader("X-Another-Header").getValue());
} finally {
manager.close();
}
}

@Test
public void testCustomHeadersOverrideSdkHeaders() throws Exception {
Map<String, String> customHeaders = new HashMap<>();
// Override the Authorization header
customHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer custom-token");

HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withDatafileAccessToken("sdk-token")
.withCustomHeaders(customHeaders)
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Verify custom header overrides SDK header
assertNotNull(request.getFirstHeader(HttpHeaders.AUTHORIZATION));
assertEquals("Bearer custom-token", request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue());
} finally {
manager.close();
}
}

@Test
public void testWithoutCustomHeaders() throws Exception {
HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Verify no custom headers are present (only SDK headers)
assertNull(request.getFirstHeader("X-Custom-Header"));
} finally {
manager.close();
}
}

@Test
public void testWithNullCustomHeaders() throws Exception {
HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withCustomHeaders(null)
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Should not throw exception and should work normally
assertNotNull(request);
} finally {
manager.close();
}
}

@Test
public void testWithEmptyCustomHeaders() throws Exception {
Map<String, String> customHeaders = new HashMap<>();

HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withCustomHeaders(customHeaders)
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Should not throw exception and should work normally
assertNotNull(request);
} finally {
manager.close();
}
}

@Test
public void testCustomHeadersWithDatafileAccessToken() throws Exception {
Map<String, String> customHeaders = new HashMap<>();
customHeaders.put("X-Custom-Header", "custom-value");

HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withDatafileAccessToken("test-token")
.withCustomHeaders(customHeaders)
.build(true);

try {
HttpGet request = manager.createHttpRequest();

// Verify both custom headers and SDK headers are present
assertNotNull(request.getFirstHeader("X-Custom-Header"));
assertEquals("custom-value", request.getFirstHeader("X-Custom-Header").getValue());
assertNotNull(request.getFirstHeader(HttpHeaders.AUTHORIZATION));
assertEquals("Bearer test-token", request.getFirstHeader(HttpHeaders.AUTHORIZATION).getValue());
} finally {
manager.close();
}
}

@Test
public void testCustomHeadersAreImmutable() throws Exception {
Map<String, String> customHeaders = new HashMap<>();
customHeaders.put("X-Custom-Header", "original-value");

HttpProjectConfigManager manager = builder()
.withOptimizelyHttpClient(mockHttpClient)
.withSdkKey("test-sdk-key")
.withCustomHeaders(customHeaders)
.build(true);

try {
// Modify the original map after building
customHeaders.put("X-Custom-Header", "modified-value");
customHeaders.put("X-New-Header", "new-value");

HttpGet request = manager.createHttpRequest();

// Verify headers are not affected by external map modifications
assertEquals("original-value", request.getFirstHeader("X-Custom-Header").getValue());
assertNull(request.getFirstHeader("X-New-Header"));
} finally {
manager.close();
}
}
}