From 88e724eb24e7832ea3293dad44459d419eb53d41 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:50 +0000 Subject: [PATCH 1/7] Decode test HTTP bodies as UTF-8 regardless of the platform charset TestContext.get()/post() and MockIpdataServer.handleBulk() wrapped HTTP bodies in an InputStreamReader without a charset, and MappingTest read fixture.json the same way, so on a JVM whose default charset is not UTF-8 (JDK 11 under a POSIX locale) the emoji flags and currency symbols served by the mock came back mangled and 11 tests failed. All of these now decode with StandardCharsets.UTF_8 explicitly; TestContext.post() sends its body as UTF-8 too, and the mock server uses the same constant for its response bytes and for query-string decoding. Co-Authored-By: Claude Fable 5.1 --- src/test/java/io/ipdata/client/MappingTest.java | 3 ++- src/test/java/io/ipdata/client/MockIpdataServer.java | 12 ++++-------- src/test/java/io/ipdata/client/TestContext.java | 10 ++++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/test/java/io/ipdata/client/MappingTest.java b/src/test/java/io/ipdata/client/MappingTest.java index 0d5c5fd..77eb722 100644 --- a/src/test/java/io/ipdata/client/MappingTest.java +++ b/src/test/java/io/ipdata/client/MappingTest.java @@ -8,6 +8,7 @@ import org.junit.Test; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; public class MappingTest { @@ -16,7 +17,7 @@ public class MappingTest { @Test @SneakyThrows public void testMapping(){ String actual = TEST_CONTEXT.mapper().writeValueAsString(TEST_CONTEXT.mapper().readValue(getClass().getResourceAsStream("fixture.json"), IpdataModel.class)); - String expected = CharStreams.toString(new InputStreamReader(getClass().getResourceAsStream("fixture.json"))); + String expected = CharStreams.toString(new InputStreamReader(getClass().getResourceAsStream("fixture.json"), StandardCharsets.UTF_8)); TEST_CONTEXT.assertEqualJson(actual, expected, Configuration.empty().withOptions(Option.TREATING_NULL_AS_ABSENT).whenIgnoringPaths("languages[0].rtl")); } diff --git a/src/test/java/io/ipdata/client/MockIpdataServer.java b/src/test/java/io/ipdata/client/MockIpdataServer.java index e8c87e9..f0e06a2 100644 --- a/src/test/java/io/ipdata/client/MockIpdataServer.java +++ b/src/test/java/io/ipdata/client/MockIpdataServer.java @@ -11,9 +11,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -212,7 +212,7 @@ private void handleGet(HttpExchange exchange, String path, Map p } private void handleBulk(HttpExchange exchange) throws IOException { - String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody())); + String body = CharStreams.toString(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8)); String[] ips = mapper.readValue(body, String[].class); ArrayNode result = mapper.createArrayNode(); for (String ip : ips) { @@ -225,7 +225,7 @@ private void handleBulk(HttpExchange exchange) throws IOException { } private void sendResponse(HttpExchange exchange, int status, String body) throws IOException { - byte[] bytes = body.getBytes("UTF-8"); + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(status, bytes.length); exchange.getResponseBody().write(bytes); @@ -243,11 +243,7 @@ private Map parseQuery(String rawQuery) { for (String param : rawQuery.split("&")) { String[] pair = param.split("=", 2); if (pair.length == 2) { - try { - params.put(URLDecoder.decode(pair[0], "UTF-8"), URLDecoder.decode(pair[1], "UTF-8")); - } catch (UnsupportedEncodingException e) { - params.put(pair[0], pair[1]); - } + params.put(URLDecoder.decode(pair[0], StandardCharsets.UTF_8), URLDecoder.decode(pair[1], StandardCharsets.UTF_8)); } } } diff --git a/src/test/java/io/ipdata/client/TestContext.java b/src/test/java/io/ipdata/client/TestContext.java index c367d3b..8bf26d1 100644 --- a/src/test/java/io/ipdata/client/TestContext.java +++ b/src/test/java/io/ipdata/client/TestContext.java @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Map; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; @@ -71,7 +72,8 @@ void assertEqualJson(String actual, String expected, Configuration configuration } /* - Used to get responses of a raw http call, without logical proxying + Used to get responses of a raw http call, without logical proxying. + Bodies are always decoded as UTF-8, whatever the platform default charset is. */ @SneakyThrows String get(String path, Map params) { @@ -84,7 +86,7 @@ String get(String path, Map params) { } HttpGet httpget = new HttpGet(builder.build()); HttpResponse response = httpClient.execute(httpget); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); } @SneakyThrows @@ -97,9 +99,9 @@ String post(String path, String content, Map params) { } } HttpPost httpPost = new HttpPost(builder.build()); - httpPost.setEntity(new StringEntity(content)); + httpPost.setEntity(new StringEntity(content, StandardCharsets.UTF_8)); HttpResponse response = httpClient.execute(httpPost); - return CharStreams.toString(new InputStreamReader(response.getEntity().getContent())); + return CharStreams.toString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8)); } From 57d606e68720621b7edfb61c8474c89d55b94a56 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:50 +0000 Subject: [PATCH 2/7] Map the languages[].code response attribute The API returns a code for every entry of languages[] but the Language model had no field for it: the lenient client mapper silently dropped the value and the strict test mapper would have rejected any fixture carrying it. Add Language.code() (null when the API does not send it), add the attribute to the test fixtures and cover it with LanguageTest (full model, field selection, absent value, strict mapping). Co-Authored-By: Claude Fable 5.1 --- .../java/io/ipdata/client/model/Language.java | 2 + .../java/io/ipdata/client/LanguageTest.java | 80 +++++++++++++++++++ .../resources/io/ipdata/client/fixture.json | 3 +- .../io/ipdata/client/fixtures/1.1.1.1.json | 3 +- .../ipdata/client/fixtures/187.13.250.1.json | 3 +- .../client/fixtures/2001-4860-4860--8844.json | 3 +- .../client/fixtures/2001-4860-4860--8888.json | 3 +- .../ipdata/client/fixtures/41.128.21.123.json | 3 +- .../io/ipdata/client/fixtures/8.8.8.8.json | 3 +- .../io/ipdata/client/fixtures/98.97.34.0.json | 3 +- 10 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 src/test/java/io/ipdata/client/LanguageTest.java diff --git a/src/main/java/io/ipdata/client/model/Language.java b/src/main/java/io/ipdata/client/model/Language.java index 395358c..a300465 100644 --- a/src/main/java/io/ipdata/client/model/Language.java +++ b/src/main/java/io/ipdata/client/model/Language.java @@ -10,5 +10,7 @@ public class Language { private String name; @JsonProperty("native") private String nativeName; + /** The language code, for example {@code en} for English; {@code null} when the API does not send it. */ + private String code; private int rtl; } diff --git a/src/test/java/io/ipdata/client/LanguageTest.java b/src/test/java/io/ipdata/client/LanguageTest.java new file mode 100644 index 0000000..39422ac --- /dev/null +++ b/src/test/java/io/ipdata/client/LanguageTest.java @@ -0,0 +1,80 @@ +package io.ipdata.client; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.ipdata.client.model.IpdataModel; +import io.ipdata.client.model.Language; +import io.ipdata.client.service.IpdataService; +import lombok.SneakyThrows; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +import static io.ipdata.client.service.IpdataField.LANGUAGES; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * The {@code code} attribute of the {@code languages[]} entries. + */ +public class LanguageTest { + + private static final MockIpdataServer MOCK = MockIpdataServer.getInstance(); + private static final TestContext TEST_CONTEXT = new TestContext(MockIpdataServer.API_KEY, MOCK.getUrl()); + + private static List services() { + return Arrays.asList(TEST_CONTEXT.ipdataService(), TEST_CONTEXT.cachingIpdataService()); + } + + private static Language onlyLanguage(IpdataModel model) { + assertEquals(1, model.languages().size()); + return model.languages().get(0); + } + + @Test + @SneakyThrows + public void codeIsMappedOnTheFullModel() { + for (IpdataService service : services()) { + Language english = onlyLanguage(service.ipdata("8.8.8.8")); + assertEquals("English", english.name()); + assertEquals("English", english.nativeName()); + assertEquals("en", english.code()); + assertEquals("pt", onlyLanguage(service.ipdata("187.13.250.1")).code()); + assertEquals("ar", onlyLanguage(service.ipdata("41.128.21.123")).code()); + } + } + + @Test + @SneakyThrows + public void codeIsMappedOnFieldSelection() { + for (IpdataService service : services()) { + IpdataModel model = service.getFields("8.8.8.8", LANGUAGES); + assertEquals("en", onlyLanguage(model).code()); + assertNull(model.city()); + } + } + + @Test + @SneakyThrows + public void codeIsNullWhenAbsent() { + String ip = "203.0.113.8"; + ObjectNode response = (ObjectNode) TEST_CONTEXT.mapper().readTree(TEST_CONTEXT.get("/8.8.8.8", null)); + response.put("ip", ip); + ((ObjectNode) response.get("languages").get(0)).remove("code"); + MOCK.registerFixture(ip, response.toString()); + + for (IpdataService service : services()) { + Language language = onlyLanguage(service.ipdata(ip)); + assertEquals("English", language.name()); + assertNull(language.code()); + } + } + + @Test + @SneakyThrows + public void strictMapperAcceptsTheCodeAttribute() { + // the test mapper fails on unknown properties, so every attribute the fixtures carry must exist on the model + IpdataModel model = TEST_CONTEXT.mapper().readValue(TEST_CONTEXT.get("/8.8.8.8", null), IpdataModel.class); + assertEquals("en", onlyLanguage(model).code()); + } +} diff --git a/src/test/resources/io/ipdata/client/fixture.json b/src/test/resources/io/ipdata/client/fixture.json index f5ecb49..3ece9f8 100644 --- a/src/test/resources/io/ipdata/client/fixture.json +++ b/src/test/resources/io/ipdata/client/fixture.json @@ -39,7 +39,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json index 11e2002..3475de5 100644 --- a/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json +++ b/src/test/resources/io/ipdata/client/fixtures/1.1.1.1.json @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json b/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json index 3e179dc..685d07f 100644 --- a/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json +++ b/src/test/resources/io/ipdata/client/fixtures/187.13.250.1.json @@ -32,7 +32,8 @@ "languages": [ { "name": "Portuguese", - "native": "Português" + "native": "Português", + "code": "pt" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json index 62b19c9..fd66fe7 100644 --- a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json +++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8844.json @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json index 766ef8c..5ba5553 100644 --- a/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json +++ b/src/test/resources/io/ipdata/client/fixtures/2001-4860-4860--8888.json @@ -37,7 +37,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json index 58c661a..211ff33 100644 --- a/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json +++ b/src/test/resources/io/ipdata/client/fixtures/41.128.21.123.json @@ -37,7 +37,8 @@ "languages": [ { "name": "Arabic", - "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629" + "native": "\u0627\u0644\u0639\u0631\u0628\u064a\u0629", + "code": "ar" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json index 4a7013d..a3a2666 100644 --- a/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json +++ b/src/test/resources/io/ipdata/client/fixtures/8.8.8.8.json @@ -38,7 +38,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { diff --git a/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json b/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json index 2f780b5..c17c0c7 100644 --- a/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json +++ b/src/test/resources/io/ipdata/client/fixtures/98.97.34.0.json @@ -33,7 +33,8 @@ "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en" } ], "currency": { From 41b2376480688e1df3b3b0bc69328a6f67996059 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:50 +0000 Subject: [PATCH 3/7] Replace deprecated Feign API calls Ipv6SafeClient rebuilt the request with the deprecated Request.create(HttpMethod, String, Map, byte[], Charset), which also dropped the RequestTemplate; it now uses the overload that takes the template. FieldDecoder and ApiErrorDecoder read bodies through the deprecated Response.Body.asReader(), which is UTF-8 underneath; they now pass StandardCharsets.UTF_8 explicitly. Same behaviour, and the main sources compile without deprecation warnings. Co-Authored-By: Claude Fable 5.1 --- src/main/java/io/ipdata/client/service/ApiErrorDecoder.java | 3 ++- src/main/java/io/ipdata/client/service/FieldDecoder.java | 3 ++- src/main/java/io/ipdata/client/service/Ipv6SafeClient.java | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java index e526eb7..c2fcea1 100644 --- a/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java +++ b/src/main/java/io/ipdata/client/service/ApiErrorDecoder.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; @RequiredArgsConstructor @Slf4j @@ -30,7 +31,7 @@ public Exception decode(String methodKey, Response response) { URL url = new URL(response.request().url()); logger.error("An error occurred during an Ipdata API call: got status '{}' for path '{}'", responseCode, url.getPath()); - message = CharStreams.toString(response.body().asReader()); + message = CharStreams.toString(response.body().asReader(StandardCharsets.UTF_8)); Error error = mapper.readValue(message, Error.class); if (responseCode == RATE_LIMIT_STATUS) { return new RateLimitException(error.getMessage(), responseCode); diff --git a/src/main/java/io/ipdata/client/service/FieldDecoder.java b/src/main/java/io/ipdata/client/service/FieldDecoder.java index 1eb1b58..fb0bd38 100644 --- a/src/main/java/io/ipdata/client/service/FieldDecoder.java +++ b/src/main/java/io/ipdata/client/service/FieldDecoder.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; /** * The API returns usable but invalid(/unquoted) strings for String fields @@ -22,7 +23,7 @@ class FieldDecoder implements Decoder { @Override public Object decode(Response response, Type type) throws IOException { if (type.equals(String.class)) { - return CharStreams.toString(response.body().asReader()); + return CharStreams.toString(response.body().asReader(StandardCharsets.UTF_8)); } return mapper.readValue(response.body().asInputStream(), mapper.constructType(type)); } diff --git a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java index f8f6f38..917ee4b 100644 --- a/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java +++ b/src/main/java/io/ipdata/client/service/Ipv6SafeClient.java @@ -36,7 +36,7 @@ public Response execute(Request request, Request.Options options) throws IOExcep String fixedUrl = fixedPath + query; request = Request.create( request.httpMethod(), fixedUrl, request.headers(), - request.body(), request.charset() + request.body(), request.charset(), request.requestTemplate() ); } return delegate.execute(request, options); From b17b4ba64171dc1cb2c42f92b8345d3e1811a517 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:51 +0000 Subject: [PATCH 4/7] Name the delegate getters in IpdataServiceSupport after what they return getSingleFieldClient() returned the full-model client and getApi() the single-field client. Both exist only to feed Lombok's @Delegate on a package-private class, so the names are swapped and both getters made private; no generated method and no public API changes. Co-Authored-By: Claude Fable 5.1 --- .../java/io/ipdata/client/service/IpdataServiceSupport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java index 7531ba8..78982b5 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceSupport.java @@ -21,12 +21,12 @@ class IpdataServiceSupport implements IpdataService { private final IpdataInternalSingleFieldClient singleFieldClient; @Delegate(types = IpdataInternalClient.class) - public IpdataInternalClient getSingleFieldClient() { + private IpdataInternalClient getApi() { return api; } @Delegate(types = IpdataInternalSingleFieldClient.class) - private IpdataInternalSingleFieldClient getApi() { + private IpdataInternalSingleFieldClient getSingleFieldClient() { return singleFieldClient; } From 20364513cdb08914cef0623cd052888129bd8812 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:51 +0000 Subject: [PATCH 5/7] Remove the unused polyglot-groovy build extension .mvn/extensions.xml declared io.takari.polyglot:polyglot-groovy 0.4.3, which lets Maven read a pom.groovy. The project uses a plain pom.xml, so the extension and its Groovy runtime were downloaded on every fresh build and never used. Co-Authored-By: Claude Fable 5.1 --- .mvn/extensions.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .mvn/extensions.xml diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml deleted file mode 100644 index 3abb8f2..0000000 --- a/.mvn/extensions.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - io.takari.polyglot - polyglot-groovy - 0.4.3 - - - From c7b6446bdefe2cfb0da33012ee9e2f86876a0b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:05:39 +0000 Subject: [PATCH 6/7] Fix README table of contents, heading level and sample output The table of contents linked "API" to #create-an-instance, an anchor that does not exist; "Bulk data" was a level-3 heading although it belongs to the API section; the "Basic Usage" sample output predated carrier, is_satellite, is_anycast, languages[].code, threat.is_vpn, threat.blocklists and threat.scores. The sample is now the model serialized from the 1.1.1.1 test fixture, and languages[].code is listed with the other recently added fields. Co-Authored-By: Claude Fable 5.1 --- README.md | 79 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3a0d557..7c1176b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An 100% compliant [ipdata.co](https://ipdata.co) API java client. - [Install](#install) - [Use](#use) - [Configuration](#configuration) - - [API](#create-an-instance) + - [API](#api) - [Basic Usage](#basic-usage) - [Connection reuse and HTTP/2](#connection-reuse-and-http2) - [Single Field Selection](#single-field-selection) @@ -81,30 +81,38 @@ Output: { "ip": "1.1.1.1", "is_eu": false, - "city": null, - "region": null, - "region_code": null, - "region_type": null, - "country_name": "Australia", - "country_code": "AU", - "continent_name": "Oceania", - "continent_code": "OC", - "latitude": -33.494, - "longitude": 143.2104, - "postal": null, - "calling_code": "61", - "flag": "https://ipdata.co/flags/au.png", - "emoji_flag": "\ud83c\udde6\ud83c\uddfa", - "emoji_unicode": "U+1F1E6 U+1F1FA", + "is_satellite": false, + "is_anycast": false, + "city": "Los Angeles", + "organisation": null, + "region": "California", + "region_code": "CA", + "region_type": "state", + "country_name": "United States", + "country_code": "US", + "continent_name": "North America", + "continent_code": "NA", + "latitude": 34.0522, + "longitude": -118.2437, + "postal": "90001", + "calling_code": "1", + "flag": "https://ipdata.co/flags/us.png", + "emoji_flag": "\ud83c\uddfa\ud83c\uddf8", + "emoji_unicode": "U+1F1FA U+1F1F8", "asn": { "asn": "AS13335", - "name": "Cloudflare, Inc.", + "name": "Cloudflare Inc", "domain": "cloudflare.com", "route": "1.1.1.0/24", "type": "hosting" }, + "carrier": { + "name": "Cloudflare", + "mcc": "310", + "mnc": "000" + }, "company": { - "name": "Cloudflare, Inc.", + "name": "Cloudflare Inc", "domain": "cloudflare.com", "network": "1.1.1.0/24", "type": "hosting" @@ -112,25 +120,28 @@ Output: "languages": [ { "name": "English", - "native": "English" + "native": "English", + "code": "en", + "rtl": 0 } ], "currency": { - "name": "Australian Dollar", - "code": "AUD", - "symbol": "AU$", + "name": "US Dollar", + "code": "USD", + "symbol": "$", "native": "$", - "plural": "Australian dollars" + "plural": "US dollars" }, "time_zone": { - "name": "Australia/Sydney", - "abbr": "AEDT", - "offset": "+1100", + "name": "America/Los_Angeles", + "abbr": "PDT", + "offset": "-0700", "is_dst": true, - "current_time": "2020-01-29T20:22:52.283874+11:00" + "current_time": "2020-06-12T06:37:16.595612-07:00" }, "threat": { "is_tor": false, + "is_vpn": false, "is_proxy": false, "is_anonymous": false, "is_known_attacker": false, @@ -138,9 +149,16 @@ Output: "is_threat": false, "is_bogon": false, "is_icloud_relay": false, - "is_datacenter": false + "is_datacenter": false, + "blocklists": [], + "scores": { + "vpn_score": 0, + "proxy_score": 0, + "threat_score": 0, + "trust_score": 100 + } }, - "count": "0" + "count": "1500" } ``` @@ -151,6 +169,7 @@ The following fields were added to the API responses recently and are mapped by - `threat.blocklists[].first_seen` – `Blocklist.getFirstSeen()`: the first day the address was seen on that blocklist, as a `YYYY-MM-DD` (UTC) string, or `null` when the feed does not report it. - `threat.blocklists[].last_seen` – `Blocklist.getLastSeen()`: the most recent such day, same format, or `null`. - `threat.blocklists[].days_seen` – `Blocklist.getDaysSeen()`: the number of days the address was seen on that blocklist, as an `Integer`, or `null`. +- `languages[].code` – `Language.code()`: the code of the language (`en` for English), or `null` when the API does not send it. #### Connection reuse and HTTP/2 An `IpdataService` keeps a persistent HTTPS connection pool for as long as it lives. Create one service per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new service pays a fresh TLS handshake. Requests to api.ipdata.co are multiplexed over HTTP/2 when the platform negotiates it, and fall back to HTTP/1.1 keep-alive otherwise. @@ -206,7 +225,7 @@ IpdataModel flags = ipdataService.getFields("1.1.1.1", IpdataField.IS_SATELLITE, ``` -### Bulk data +#### Bulk data You can as well get multiple responses at once by using the ``bulk`` api: ```java From 7c91a05b5b53aeb924763e276ad49bae4aed6fdd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:08:24 +0000 Subject: [PATCH 7/7] Bump Guava to 33.7.2 and declare Jackson 2.22.4 directly Guava 33.4.8 is affected by GHSA-xxph-c9ww-hj94 (CVE-2026-102554: uncontrolled memory allocation when deserializing CompactHashMap, CompactHashSet or MapMakerInternalMap); 33.7.2 is the first patched version. The project compiles against Jackson directly (ObjectMapper, @JsonProperty) but only received it through feign-jackson, which pins jackson-databind 2.13.4, affected by CVE-2022-42003 (and jackson-core 2.13.4 by CVE-2025-52999). jackson-databind 2.22.4 and jackson-annotations 2.22 (annotations are released once per minor line since 2.20) are now declared as direct dependencies, which also makes them visible to the repository's dependency graph. The new versions deprecate two calls in IpdataServiceBuilder; they are replaced by their documented equivalents, available since Jackson 2.7 and Guava 25: ObjectMapper.setDefaultPropertyInclusion(Include) for setSerializationInclusion(Include), and CacheBuilder.expireAfterWrite(Duration) for expireAfterWrite(long, TimeUnit). Co-Authored-By: Claude Fable 5.1 --- pom.xml | 15 ++++++++++++++- .../client/service/IpdataServiceBuilder.java | 16 +++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index b20011d..b242050 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,9 @@ 0.8.14 3.5.5 11.10 + 2.22.4 + + 2.22 @@ -57,6 +60,16 @@ feign-java11 ${version.client.feign} + + com.fasterxml.jackson.core + jackson-databind + ${version.client.jackson} + + + com.fasterxml.jackson.core + jackson-annotations + ${version.client.jackson.annotations} + io.github.openfeign feign-httpclient @@ -66,7 +79,7 @@ com.google.guava guava - 33.4.8-jre + 33.7.2-jre org.slf4j diff --git a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java index b22ef6d..22bcc40 100644 --- a/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java +++ b/src/main/java/io/ipdata/client/service/IpdataServiceBuilder.java @@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory; import java.net.URL; +import java.time.Duration; import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SNAKE_CASE; @@ -64,7 +65,7 @@ public static IpdataServiceBuilder of(URL url, String key, CacheConfig cacheConf public IpdataService build() { final ObjectMapper mapper = new ObjectMapper(); mapper.setPropertyNamingStrategy(SNAKE_CASE); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); @@ -98,13 +99,14 @@ public IpdataService build() { .target(IpdataInternalSingleFieldClient.class, url.toString()); if (cacheConfig != null) { + final Duration expiry = Duration.of(cacheConfig.timeout(), cacheConfig.unit().toChronoUnit()); CachingInternalClient cachingInternalClient = CachingInternalClient.builder() .expiry(cacheConfig.timeout()) .unit(cacheConfig.unit()) .maxSize(cacheConfig.maxSize()) .ipdataCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -115,7 +117,7 @@ public IpdataModel load(String key) throws Exception { ) .fieldsCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader, IpdataModel>() { @Override @@ -126,7 +128,7 @@ public IpdataModel load(HashPair key) throws Exception { ) .asnCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -137,7 +139,7 @@ public AsnModel load(String key) throws Exception { ) .tzCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -148,7 +150,7 @@ public TimeZone load(String key) throws Exception { ) .currencyCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override @@ -159,7 +161,7 @@ public Currency load(String key) throws Exception { ) .threatCache( CacheBuilder.newBuilder() - .expireAfterWrite(cacheConfig.timeout(), cacheConfig.unit()) + .expireAfterWrite(expiry) .maximumSize(cacheConfig.maxSize()) .build(new CacheLoader() { @Override