diff --git a/README.md b/README.md index 1f4da74cc..251adb074 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## LaunchDarkly overview -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) @@ -54,7 +54,7 @@ Unlike some other languages, in Java the DNS caching behavior is controlled by t ## Learn more -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/java-sdk-reference) or our [code-generated API documentation](https://launchdarkly.github.io/java-server-sdk/). +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/java-sdk-reference) or our [code-generated API documentation](https://launchdarkly.github.io/java-server-sdk/). ## Testing diff --git a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java index 53d36aa46..50494f99d 100644 --- a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java @@ -39,6 +39,8 @@ import java.net.Proxy; import java.net.URI; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -325,10 +327,12 @@ static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder @Override public HttpConfiguration build(ClientContext clientContext) { LDLogger logger = clientContext.getBaseLogger(); + // Build the default headers - ImmutableMap.Builder headers = ImmutableMap.builder(); + Map headers = new HashMap<>(); headers.put("Authorization", clientContext.getSdkKey()); headers.put("User-Agent", "JavaClient/" + Version.SDK_VERSION); + if (clientContext.getApplicationInfo() != null) { String tagHeader = Util.applicationTagHeader(clientContext.getApplicationInfo(), logger); if (!tagHeader.isEmpty()) { @@ -337,14 +341,18 @@ public HttpConfiguration build(ClientContext clientContext) { } if (wrapperName != null) { String wrapperId = wrapperVersion == null ? wrapperName : (wrapperName + "/" + wrapperVersion); - headers.put("X-LaunchDarkly-Wrapper", wrapperId); + headers.put("X-LaunchDarkly-Wrapper", wrapperId); + } + + if (!customHeaders.isEmpty()) { + headers.putAll(customHeaders); } Proxy proxy = proxyHost == null ? null : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); return new HttpConfiguration( connectTimeout, - headers.build(), + headers, proxy, proxyAuth, socketFactory, diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java index e7ed3ad48..6bb569ba1 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java @@ -6,6 +6,8 @@ import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; @@ -45,6 +47,7 @@ public abstract class HttpConfigurationBuilder implements ComponentConfigurer customHeaders = new HashMap<>(); protected int proxyPort; protected Duration socketTimeout = DEFAULT_SOCKET_TIMEOUT; protected SocketFactory socketFactory; @@ -137,6 +140,21 @@ public HttpConfigurationBuilder sslSocketFactory(SSLSocketFactory sslSocketFacto return this; } + /** + * Specifies a custom HTTP header that should be added to all SDK requests. + *

+ * This may be helpful if you are using a gateway or proxy server that requires a specific header in requests. You + * may add any number of headers. + * + * @param headerName standard HTTP header + * @param headerValue standard HTTP value + * @return the builder + */ + public HttpConfigurationBuilder addCustomHeader(String headerName, String headerValue) { + this.customHeaders.put(headerName, headerValue); + 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 diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java index 237de74dc..2c41c6727 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java @@ -34,13 +34,13 @@ public class HttpConfigurationBuilderTest { private static final String SDK_KEY = "sdk-key"; private static final ClientContext BASIC_CONTEXT = new ClientContext(SDK_KEY); - + private static ImmutableMap.Builder buildBasicHeaders() { return ImmutableMap.builder() .put("Authorization", SDK_KEY) .put("User-Agent", "JavaClient/" + getSdkVersion()); } - + @Test public void testDefaults() { HttpConfiguration hc = Components.httpConfiguration().build(BASIC_CONTEXT); @@ -54,6 +54,25 @@ public void testDefaults() { assertEquals(buildBasicHeaders().build(), ImmutableMap.copyOf(hc.getDefaultHeaders())); } + @Test + public void testCanSetCustomHeaders() { + HttpConfiguration hc = Components.httpConfiguration() + .addCustomHeader("X-LaunchDarkly-Test-Label", "my-cool-label") + .addCustomHeader("X-Header-Message", "Java FTW") + .addCustomHeader("Authorization", "I can override this") + .addCustomHeader("User-Agent", "This too") + .build(BASIC_CONTEXT); + + ImmutableMap expectedHeaders = ImmutableMap.builder() + .put("X-LaunchDarkly-Test-Label", "my-cool-label") + .put("X-Header-Message", "Java FTW") + .put("Authorization", "I can override this") + .put("User-Agent", "This too") + .build(); + + assertEquals(expectedHeaders, ImmutableMap.copyOf(hc.getDefaultHeaders())); + } + @Test public void testConnectTimeout() { HttpConfiguration hc = Components.httpConfiguration() @@ -101,7 +120,7 @@ public void testSocketTimeout() { .build(BASIC_CONTEXT); assertEquals(DEFAULT_SOCKET_TIMEOUT, hc2.getSocketTimeout()); } - + @Test public void testSocketFactory() { SocketFactory sf = new StubSocketFactory(); @@ -110,7 +129,7 @@ public void testSocketFactory() { .build(BASIC_CONTEXT); assertSame(sf, hc.getSocketFactory()); } - + @Test public void testSslOptions() { SSLSocketFactory sf = new StubSSLSocketFactory(); @@ -146,22 +165,22 @@ public void testApplicationTags() { .build(contextWithTags); assertEquals("application-id/authentication-service application-version/1.0.0", ImmutableMap.copyOf(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); } - + public static class StubSocketFactory extends SocketFactory { public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return null; } - + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return null; } - + public Socket createSocket(InetAddress host, int port) throws IOException { return null; } - + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { return null; } @@ -176,40 +195,40 @@ public Socket createSocket(InetAddress address, int port, InetAddress localAddre throws IOException { return null; } - + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { return null; } - + public Socket createSocket(InetAddress host, int port) throws IOException { return null; } - + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { return null; } - + public String[] getSupportedCipherSuites() { return null; } - + public String[] getDefaultCipherSuites() { return null; } - + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return null; } } - + public static class StubX509TrustManager implements X509TrustManager { public X509Certificate[] getAcceptedIssuers() { return null; } - + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} - + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} } }