From e075cc5e24b723e6913accbba2a5063f9b593a60 Mon Sep 17 00:00:00 2001 From: William Storey Date: Mon, 25 Aug 2025 16:03:37 +0000 Subject: [PATCH 1/2] Add httpClient() method to WebServiceClient.Builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows users to provide a custom HttpClient for full control over client configuration. When a custom HttpClient is provided, the builder validates that conflicting parameters (connectTimeout, proxy) are not also set, ensuring clear configuration ownership. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 6 +++ .../com/maxmind/geoip2/WebServiceClient.java | 42 +++++++++++++-- .../maxmind/geoip2/WebServiceClientTest.java | 53 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2ae5e3..f7ea3b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +4.4.0 +------------------ + +* `WebServiceClient.Builder` now has an `httpClient()` method to allow + passing in a custom `HttpClient`. + 4.3.1 (2025-05-28) ------------------ diff --git a/src/main/java/com/maxmind/geoip2/WebServiceClient.java b/src/main/java/com/maxmind/geoip2/WebServiceClient.java index 2fbad5a9..fdc779e0 100644 --- a/src/main/java/com/maxmind/geoip2/WebServiceClient.java +++ b/src/main/java/com/maxmind/geoip2/WebServiceClient.java @@ -140,10 +140,16 @@ private WebServiceClient(Builder builder) { .build(); requestTimeout = builder.requestTimeout; - httpClient = HttpClient.newBuilder() - .connectTimeout(builder.connectTimeout) - .proxy(builder.proxy) - .build(); + + // Use custom HttpClient if provided, otherwise create a default one + if (builder.httpClient != null) { + httpClient = builder.httpClient; + } else { + httpClient = HttpClient.newBuilder() + .connectTimeout(builder.connectTimeout) + .proxy(builder.proxy) + .build(); + } } /** @@ -176,6 +182,7 @@ public static final class Builder { List locales = Collections.singletonList("en"); private ProxySelector proxy = ProxySelector.getDefault(); + private HttpClient httpClient = null; /** * @param accountId Your MaxMind account ID. @@ -296,11 +303,38 @@ public Builder proxy(ProxySelector val) { return this; } + /** + * @param val the custom HttpClient to use for requests. When providing a + * custom HttpClient, you cannot also set connectTimeout or proxy + * parameters as these should be configured on the provided client. + * @return Builder object + */ + public Builder httpClient(HttpClient val) { + this.httpClient = val; + return this; + } + /** * @return an instance of {@code WebServiceClient} created from the * fields set on this builder. + * @throws IllegalArgumentException if httpClient is provided along with + * connectTimeout or proxy settings */ public WebServiceClient build() { + if (httpClient != null) { + // Check if connectTimeout was changed from default + if (!connectTimeout.equals(Duration.ofSeconds(3))) { + throw new IllegalArgumentException( + "Cannot set both httpClient and connectTimeout. " + + "Configure timeout on the provided HttpClient instead."); + } + // Check if proxy was changed from default + if (proxy != ProxySelector.getDefault()) { + throw new IllegalArgumentException( + "Cannot set both httpClient and proxy. " + + "Configure proxy on the provided HttpClient instead."); + } + } return new WebServiceClient(this); } } diff --git a/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java b/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java index d2ee3075..9d79a37b 100644 --- a/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java +++ b/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java @@ -36,7 +36,11 @@ import com.maxmind.geoip2.record.Traits; import java.io.UnsupportedEncodingException; import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.http.HttpClient; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.List; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Test; @@ -407,4 +411,53 @@ private WebServiceClient createClient(String service, String ip, int status, Str .disableHttps() .build(); } + + @Test + public void testHttpClientWithConnectTimeoutThrowsException() { + HttpClient customClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + WebServiceClient.Builder builder = new WebServiceClient.Builder(6, "0123456789") + .httpClient(customClient) + .connectTimeout(Duration.ofSeconds(5)); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); + assertEquals("Cannot set both httpClient and connectTimeout. Configure timeout on the provided HttpClient instead.", + ex.getMessage()); + } + + @Test + public void testHttpClientWithProxyThrowsException() { + HttpClient customClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + ProxySelector proxySelector = ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)); + WebServiceClient.Builder builder = new WebServiceClient.Builder(6, "0123456789") + .httpClient(customClient) + .proxy(proxySelector); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::build); + assertEquals("Cannot set both httpClient and proxy. Configure proxy on the provided HttpClient instead.", + ex.getMessage()); + } + + @Test + public void testHttpClientWithDefaultSettingsDoesNotThrow() throws Exception { + HttpClient customClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + // Should not throw because we're not setting connectTimeout or proxy + WebServiceClient client = new WebServiceClient.Builder(6, "0123456789") + .host("localhost") + .port(8080) + .disableHttps() + .httpClient(customClient) + .build(); + + assertNotNull(client); + } + } From d9460a5ecd487c6fd9d80c5534f20358505988a6 Mon Sep 17 00:00:00 2001 From: William Storey Date: Mon, 25 Aug 2025 18:34:30 +0000 Subject: [PATCH 2/2] Simplify httpClient validation logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use null defaults for connectTimeout and proxy in Builder, making validation cleaner by simply checking for null values. Set defaults only when no custom HttpClient is provided. Remove unnecessary comments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../com/maxmind/geoip2/WebServiceClient.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/maxmind/geoip2/WebServiceClient.java b/src/main/java/com/maxmind/geoip2/WebServiceClient.java index fdc779e0..c612b917 100644 --- a/src/main/java/com/maxmind/geoip2/WebServiceClient.java +++ b/src/main/java/com/maxmind/geoip2/WebServiceClient.java @@ -141,7 +141,6 @@ private WebServiceClient(Builder builder) { requestTimeout = builder.requestTimeout; - // Use custom HttpClient if provided, otherwise create a default one if (builder.httpClient != null) { httpClient = builder.httpClient; } else { @@ -177,11 +176,11 @@ public static final class Builder { int port = 443; boolean useHttps = true; - Duration connectTimeout = Duration.ofSeconds(3); + Duration connectTimeout = null; Duration requestTimeout = Duration.ofSeconds(20); List locales = Collections.singletonList("en"); - private ProxySelector proxy = ProxySelector.getDefault(); + private ProxySelector proxy = null; private HttpClient httpClient = null; /** @@ -322,18 +321,23 @@ public Builder httpClient(HttpClient val) { */ public WebServiceClient build() { if (httpClient != null) { - // Check if connectTimeout was changed from default - if (!connectTimeout.equals(Duration.ofSeconds(3))) { + if (connectTimeout != null) { throw new IllegalArgumentException( "Cannot set both httpClient and connectTimeout. " + "Configure timeout on the provided HttpClient instead."); } - // Check if proxy was changed from default - if (proxy != ProxySelector.getDefault()) { + if (proxy != null) { throw new IllegalArgumentException( "Cannot set both httpClient and proxy. " + "Configure proxy on the provided HttpClient instead."); } + } else { + if (connectTimeout == null) { + connectTimeout = Duration.ofSeconds(3); + } + if (proxy == null) { + proxy = ProxySelector.getDefault(); + } } return new WebServiceClient(this); }