From 82ee889ac6c9c30918a9b7237ac38b325d08c27f Mon Sep 17 00:00:00 2001 From: Gabor Andras Date: Thu, 21 Aug 2025 22:34:08 +0300 Subject: [PATCH] Add httpProtocol method to WebServiceClient.Builder --- CHANGELOG.md | 11 ++ README.md | 185 +++++++++++++++++- pom.xml | 2 +- .../com/maxmind/geoip2/WebServiceClient.java | 33 +++- .../maxmind/geoip2/WebServiceClientTest.java | 30 +++ 5 files changed, 250 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2ae5e3..0250806b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +4.3.2 (2025-08-22) +------------------ + +* Added `httpVersion()` method on `WebServiceClient.Builder` + which allows configuring HTTP protocol version (HTTP/1.1 or HTTP/2) for web + service requests. +* Updated README.md with comprehensive HTTP protocol configuration documentation. +* Added detailed information about HTTP version settings, proxy configuration, + timeout settings, authentication, and connection management. +* Improved documentation for developers using the web service client. + 4.3.1 (2025-05-28) ------------------ diff --git a/README.md b/README.md index 247bb549..952edcd5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ To do this, add the dependency to your pom.xml: com.maxmind.geoip2 geoip2 - 4.3.1 + 4.3.2 ``` @@ -30,7 +30,7 @@ repositories { mavenCentral() } dependencies { - compile 'com.maxmind.geoip2:geoip2:4.3.1' + compile 'com.maxmind.geoip2:geoip2:4.3.2' } ``` @@ -72,6 +72,187 @@ are not created for each request. See the [API documentation](https://maxmind.github.io/GeoIP2-java/) for more details. +## HTTP Protocol Configuration ## + +The `WebServiceClient` provides extensive HTTP protocol configuration options +through the `WebServiceClient.Builder`: + +### HTTP Version ### + +You can specify the HTTP protocol version to use: + +```java +import java.net.http.HttpClient; + +// Force HTTP/1.1 +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .httpVersion(HttpClient.Version.HTTP_1_1) + .build(); + +// Force HTTP/2 +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .httpVersion(HttpClient.Version.HTTP_2) + .build(); + +// Default behavior: prefer HTTP/2 but negotiate down to HTTP/1.1 if needed +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .build(); +``` + +### HTTPS/HTTP Protocol ### + +By default, the client uses HTTPS. You can disable HTTPS for testing or proxy scenarios: + +```java +// Disable HTTPS (use HTTP instead) +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .disableHttps() + .build(); +``` + +**Note:** The minFraud Score and Insights web services require HTTPS. + +### Timeouts ### + +Configure connection and request timeouts: + +```java +import java.time.Duration; + +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .connectTimeout(Duration.ofSeconds(5)) // Connection timeout (default: 3 seconds) + .requestTimeout(Duration.ofSeconds(30)) // Request timeout (default: 20 seconds) + .build(); +``` + +### Proxy Configuration ### + +Configure HTTP proxy settings: + +```java +import java.net.InetSocketAddress; +import java.net.ProxySelector; + +// Using ProxySelector (recommended) +ProxySelector proxy = ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)); +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .proxy(proxy) + .build(); + +// Using system default proxy +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .proxy(ProxySelector.getDefault()) + .build(); +``` + +### Custom Host and Port ### + +Configure custom host and port: + +```java +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .host("custom.geoip.server.com") + .port(8443) + .build(); +``` + +### Complete HTTP Configuration Example ### + +```java +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.http.HttpClient; +import java.time.Duration; +import java.util.Arrays; + +WebServiceClient client = new WebServiceClient.Builder(42, "license_key") + .host("geoip.maxmind.com") + .port(443) + .httpVersion(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(5)) + .requestTimeout(Duration.ofSeconds(30)) + .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080))) + .locales(Arrays.asList("en", "fr", "es")) + .build(); +``` + +### HTTP Headers ### + +The client automatically sets the following HTTP headers: +- `Accept: application/json` +- `Authorization: Basic ` +- `User-Agent: GeoIP2/ (Java/)` + +### HTTP Error Handling ### + +The client handles various HTTP status codes: +- **200**: Success - response is parsed and returned +- **4xx**: Client errors - throws specific exceptions like `AddressNotFoundException`, `AuthenticationException`, etc. +- **5xx**: Server errors - throws `HttpException` +- **Other status codes**: Throws `HttpException` + +HTTP transport errors (network issues, timeouts, etc.) are thrown as `HttpException`, which extends `IOException`. + +### HTTP Client Implementation ### + +The GeoIP2 Java library uses Java's built-in `java.net.http.HttpClient` (available since Java 11) for HTTP communication. This provides: + +- **Connection pooling**: Connections are automatically reused across requests +- **HTTP/2 support**: Modern HTTP/2 protocol with fallback to HTTP/1.1 +- **Asynchronous capabilities**: Though the GeoIP2 client uses synchronous calls +- **Built-in proxy support**: System proxy settings are respected by default +- **TLS/SSL support**: Secure HTTPS connections with modern cipher suites + +### Authentication ### + +The client uses HTTP Basic Authentication with your MaxMind account credentials: + +- Credentials are Base64-encoded and sent in the `Authorization` header +- Authentication is sent with every request (no challenge-response) +- Credentials are never logged or exposed in error messages + +### Connection Management ### + +- **Thread-safe**: The `WebServiceClient` can be safely shared across multiple threads +- **Connection reuse**: The underlying HTTP client maintains a connection pool +- **Resource cleanup**: The client automatically manages connection lifecycle +- **No explicit cleanup needed**: The `close()` method is deprecated and no longer required + +### HTTP API Endpoints ### + +The client makes HTTP GET requests to the following endpoint pattern: +``` +https://geoip.maxmind.com/geoip/v2.1/{service}/{ip_address} +``` + +Where: +- `{service}` is one of: `country`, `city`, `insights` +- `{ip_address}` is the IP address to look up, or `me` for the client's IP + +Examples: +- `https://geoip.maxmind.com/geoip/v2.1/country/128.101.101.101` +- `https://geoip.maxmind.com/geoip/v2.1/city/me` +- `https://geolite.info/geoip/v2.1/country/192.168.1.1` (GeoLite2) +- `https://sandbox.maxmind.com/geoip/v2.1/insights/8.8.8.8` (Sandbox) + +### Request Format ### + +All requests are HTTP GET requests with: +- **Method**: GET +- **Content-Type**: Not applicable (no request body) +- **Accept**: `application/json` +- **Authorization**: `Basic ` +- **User-Agent**: `GeoIP2/ (Java/)` + +### Response Format ### + +All successful responses return JSON with HTTP 200 status. The JSON structure varies by service but typically includes: +- IP address information +- Geographic location data (country, city, subdivisions) +- ISP and organization data +- Confidence scores (for paid services) +- Additional metadata + ## Web Service Example ## ### Country Service ### diff --git a/pom.xml b/pom.xml index d57c2c13..65bb4c79 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.maxmind.geoip2 geoip2 - 4.3.1 + 4.3.2 jar MaxMind GeoIP2 API GeoIP2 webservice client and database reader diff --git a/src/main/java/com/maxmind/geoip2/WebServiceClient.java b/src/main/java/com/maxmind/geoip2/WebServiceClient.java index 2fbad5a9..072815e0 100644 --- a/src/main/java/com/maxmind/geoip2/WebServiceClient.java +++ b/src/main/java/com/maxmind/geoip2/WebServiceClient.java @@ -66,10 +66,11 @@ * the {@code host} method on the builder to {@code geolite.info}. To use the * Sandbox GeoIP2 web services instead of the production GeoIP2 web services, * set the {@code host} method on the builder to {@code sandbox.maxmind.com}. - * You may also set a {@code timeout} or set the {@code locales} fallback order - * using the methods on the {@code Builder}. After you have created the {@code - * WebServiceClient}, you may then call the method corresponding to a specific - * service, passing it the IP address you want to look up. + * You may also set a {@code timeout}, set the {@code locales} fallback order, + * or specify the HTTP protocol version using the methods on the {@code Builder}. + * After you have created the {@code WebServiceClient}, you may then call the + * method corresponding to a specific service, passing it the IP address you want + * to look up. *

*

* If the request succeeds, the method call will return a model class for the @@ -140,10 +141,13 @@ private WebServiceClient(Builder builder) { .build(); requestTimeout = builder.requestTimeout; - httpClient = HttpClient.newBuilder() + HttpClient.Builder httpClientBuilder = HttpClient.newBuilder() .connectTimeout(builder.connectTimeout) - .proxy(builder.proxy) - .build(); + .proxy(builder.proxy); + if (builder.httpVersion != null) { + httpClientBuilder.version(builder.httpVersion); + } + httpClient = httpClientBuilder.build(); } /** @@ -157,7 +161,7 @@ private WebServiceClient(Builder builder) { *

*

* {@code WebServiceClient client = new WebServiceClient.Builder(12,"licensekey") - * .host("geoip.maxmind.com").build();} + * .host("geoip.maxmind.com").httpVersion(HttpClient.Version.HTTP_1_1).build();} *

*

* Only the values set in the {@code Builder} constructor are required. @@ -170,6 +174,7 @@ public static final class Builder { String host = "geoip.maxmind.com"; int port = 443; boolean useHttps = true; + HttpClient.Version httpVersion; Duration connectTimeout = Duration.ofSeconds(3); Duration requestTimeout = Duration.ofSeconds(20); @@ -296,6 +301,18 @@ public Builder proxy(ProxySelector val) { return this; } + /** + * @param val the HTTP protocol version to use. If not set, the HttpClient + * will prefer HTTP/2 but will negotiate down to HTTP/1.1 if needed. + * Use HttpClient.Version.HTTP_1_1 to force HTTP/1.1 or + * HttpClient.Version.HTTP_2 to force HTTP/2. + * @return Builder object + */ + public Builder httpVersion(HttpClient.Version val) { + this.httpVersion = val; + return this; + } + /** * @return an instance of {@code WebServiceClient} created from the * fields set on this builder. diff --git a/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java b/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java index d2ee3075..2e36d79f 100644 --- a/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java +++ b/src/test/java/com/maxmind/geoip2/WebServiceClientTest.java @@ -36,6 +36,7 @@ import com.maxmind.geoip2.record.Traits; import java.io.UnsupportedEncodingException; import java.net.InetAddress; +import java.net.http.HttpClient; import java.nio.charset.StandardCharsets; import java.util.List; import org.hamcrest.CoreMatchers; @@ -352,6 +353,35 @@ public void test500() throws Exception { assertThat(ex.getMessage(), startsWith("Received a server error (500)")); } + @Test + public void testHttpVersionConfiguration() throws Exception { + // Test that HTTP version can be configured without errors + WebServiceClient clientHttp11 = new WebServiceClient.Builder(6, "0123456789") + .host("localhost") + .port(wireMock.getPort()) + .disableHttps() + .httpVersion(HttpClient.Version.HTTP_1_1) + .build(); + + WebServiceClient clientHttp2 = new WebServiceClient.Builder(6, "0123456789") + .host("localhost") + .port(wireMock.getPort()) + .disableHttps() + .httpVersion(HttpClient.Version.HTTP_2) + .build(); + + WebServiceClient clientDefault = new WebServiceClient.Builder(6, "0123456789") + .host("localhost") + .port(wireMock.getPort()) + .disableHttps() + .build(); + + // Verify that clients can be created successfully + assertNotNull(clientHttp11); + assertNotNull(clientHttp2); + assertNotNull(clientDefault); + } + private void createInsightsError(String ip, int status, String contentType, String responseContent) throws Exception { WebServiceClient client = createClient(