From 600173d6795b768d0937c59ac9ba5eee4f314be8 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 8 Oct 2025 14:24:21 +0200 Subject: [PATCH 1/3] feat: Added gzip content handling for features requests fixes #328 --- pom.xml | 18 ++++++-- .../repository/HttpFeatureFetcher.java | 42 +++++++++++++------ .../HotReloadSchedulerReuseTest.java | 33 +++++++-------- 3 files changed, 57 insertions(+), 36 deletions(-) diff --git a/pom.xml b/pom.xml index ef2976d94..6399b55d5 100644 --- a/pom.xml +++ b/pom.xml @@ -115,20 +115,20 @@ org.assertj assertj-core - 3.26.3 + 3.27.6 test org.mockito mockito-core - 4.8.1 + 5.20.0 test com.github.tomakehurst wiremock-jre8 - 2.35.2 + 3.0.1 test @@ -177,6 +177,13 @@ org.apache.maven.plugins maven-dependency-plugin 3.8.1 + + + + properties + + + org.apache.maven.plugins @@ -219,7 +226,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.13 prepare-agent @@ -410,6 +417,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.2 + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} + diff --git a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java index 53f2ab22b..b5484ae3e 100644 --- a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java +++ b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java @@ -5,15 +5,13 @@ import io.getunleash.UnleashException; import io.getunleash.event.ClientFeaturesResponse; import io.getunleash.util.UnleashConfig; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,15 +57,32 @@ private ClientFeaturesResponse getFeatureResponse( if (responseCode < 300) { etag = Optional.ofNullable(request.getHeaderField("ETag")); - - try (BufferedReader reader = - new BufferedReader( - new InputStreamReader( - (InputStream) request.getContent(), StandardCharsets.UTF_8))) { - - String clientFeatures = reader.lines().collect(Collectors.joining("\n")); - - return ClientFeaturesResponse.updated(clientFeatures); + String contentEncoding = request.getHeaderField("Content-Encoding"); + if (contentEncoding.equalsIgnoreCase("gzip")) { + /// Handle gzipped content + try (GZIPInputStream stream = new GZIPInputStream(request.getInputStream())) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len; + while ((len = stream.read(buffer)) != -1) { + outputStream.write(buffer, 0, len); + } + String clientFeatures = outputStream.toString(StandardCharsets.UTF_8); + return ClientFeaturesResponse.updated(clientFeatures); + } + } + } else { + /// Handle non-gzipped content + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader( + (InputStream) request.getContent(), + StandardCharsets.UTF_8))) { + + String clientFeatures = reader.lines().collect(Collectors.joining("\n")); + + return ClientFeaturesResponse.updated(clientFeatures); + } } } else if (followRedirect && (responseCode == HttpURLConnection.HTTP_MOVED_TEMP @@ -114,6 +129,7 @@ private HttpURLConnection openConnection(URL url) throws IOException { connection.setReadTimeout((int) this.config.getFetchTogglesReadTimeout().toMillis()); connection.setRequestProperty("Accept", "application/json"); connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept-Encoding", "gzip,deflate"); UnleashConfig.setRequestProperties(connection, this.config); etag.ifPresent(val -> connection.setRequestProperty("If-None-Match", val)); diff --git a/src/test/java/io/getunleash/HotReloadSchedulerReuseTest.java b/src/test/java/io/getunleash/HotReloadSchedulerReuseTest.java index 530ca4cbc..f93e772d0 100644 --- a/src/test/java/io/getunleash/HotReloadSchedulerReuseTest.java +++ b/src/test/java/io/getunleash/HotReloadSchedulerReuseTest.java @@ -1,25 +1,23 @@ package io.getunleash; -import io.getunleash.util.UnleashConfig; -import org.junit.jupiter.api.*; +import static org.assertj.core.api.Assertions.assertThat; +import io.getunleash.util.UnleashConfig; import java.lang.reflect.Field; import java.util.concurrent.ScheduledThreadPoolExecutor; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.*; class HotReloadSchedulerReuseTest { - private UnleashConfig baseConfig() { return UnleashConfig.builder() - .appName("hot-reload-test-app") - .instanceId("A") - .unleashAPI("http://localhost") // never hit - .synchronousFetchOnInitialisation(false) - .fetchTogglesInterval(100) - .sendMetricsInterval(100) - .build(); + .appName("hot-reload-test-app") + .instanceId("A") + .unleashAPI("http://localhost") // never hit + .synchronousFetchOnInitialisation(false) + .fetchTogglesInterval(100) + .sendMetricsInterval(100) + .build(); } private ScheduledThreadPoolExecutor currentGlobalExecutor() throws Exception { @@ -37,9 +35,7 @@ private ScheduledThreadPoolExecutor currentGlobalExecutor() throws Exception { @Test void secondClientDoesNotReuseSchedulerExecutor() throws Exception { // 1) Create first client; let it schedule background tasks - DefaultUnleash first = new DefaultUnleash( - baseConfig() - ); + DefaultUnleash first = new DefaultUnleash(baseConfig()); // Let it initialize/schedule Thread.sleep(150); @@ -57,11 +53,10 @@ void secondClientDoesNotReuseSchedulerExecutor() throws Exception { assertThat(execAfterShutdown).isNull(); // 3) "Reloaded app": create a second client in the same JVM (statics still around) - DefaultUnleash second = new DefaultUnleash( - baseConfig() - ); + DefaultUnleash second = new DefaultUnleash(baseConfig()); - // 4) Assert that the second client creates a fresh executor (not reusing the terminated one) + // 4) Assert that the second client creates a fresh executor (not reusing the terminated + // one) ScheduledThreadPoolExecutor execUsedBySecond = currentGlobalExecutor(); assertThat(execUsedBySecond).isNotNull(); assertThat(execUsedBySecond.isShutdown()).isFalse(); // it's a new one From 00418f04e56401fa463c531845702f2db80444bb Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 8 Oct 2025 14:28:45 +0200 Subject: [PATCH 2/3] Update src/main/java/io/getunleash/repository/HttpFeatureFetcher.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main/java/io/getunleash/repository/HttpFeatureFetcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java index b5484ae3e..41814c3d1 100644 --- a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java +++ b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java @@ -58,7 +58,7 @@ private ClientFeaturesResponse getFeatureResponse( if (responseCode < 300) { etag = Optional.ofNullable(request.getHeaderField("ETag")); String contentEncoding = request.getHeaderField("Content-Encoding"); - if (contentEncoding.equalsIgnoreCase("gzip")) { + if ("gzip".equalsIgnoreCase(contentEncoding)) { /// Handle gzipped content try (GZIPInputStream stream = new GZIPInputStream(request.getInputStream())) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { From 9b934117487f6d605fc8681534ab47369300f0b1 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 8 Oct 2025 14:30:44 +0200 Subject: [PATCH 3/3] fix(nit): Removed unnecessary comments --- src/main/java/io/getunleash/repository/HttpFeatureFetcher.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java index 41814c3d1..a8863bb10 100644 --- a/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java +++ b/src/main/java/io/getunleash/repository/HttpFeatureFetcher.java @@ -59,7 +59,6 @@ private ClientFeaturesResponse getFeatureResponse( etag = Optional.ofNullable(request.getHeaderField("ETag")); String contentEncoding = request.getHeaderField("Content-Encoding"); if ("gzip".equalsIgnoreCase(contentEncoding)) { - /// Handle gzipped content try (GZIPInputStream stream = new GZIPInputStream(request.getInputStream())) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; @@ -72,7 +71,6 @@ private ClientFeaturesResponse getFeatureResponse( } } } else { - /// Handle non-gzipped content try (BufferedReader reader = new BufferedReader( new InputStreamReader(