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(