From 63c38a1c004055a442f586be4e485c0b1e786bc4 Mon Sep 17 00:00:00 2001 From: Volodymyr Nazarkevych Date: Tue, 16 Jun 2026 22:33:16 +0300 Subject: [PATCH 1/2] fix: handle empty SSE event payloads --- .../java/repository/GBFeaturesRepository.java | 10 ++++-- .../NativeJavaGbFeatureRepository.java | 3 +- .../java/sse/SseEventPayloadValidator.java | 25 ++++++++++++++ .../growthbook/sdk/java/util/StringUtils.java | 9 +++++ .../growthbook/sdk/java/StringUtilsTest.java | 13 ++++++++ .../sse/SseEventPayloadValidatorTest.java | 33 +++++++++++++++++++ 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/growthbook/sdk/java/sse/SseEventPayloadValidator.java create mode 100644 lib/src/test/java/growthbook/sdk/java/sse/SseEventPayloadValidatorTest.java diff --git a/lib/src/main/java/growthbook/sdk/java/repository/GBFeaturesRepository.java b/lib/src/main/java/growthbook/sdk/java/repository/GBFeaturesRepository.java index 3fed5651..9ef6dcd4 100644 --- a/lib/src/main/java/growthbook/sdk/java/repository/GBFeaturesRepository.java +++ b/lib/src/main/java/growthbook/sdk/java/repository/GBFeaturesRepository.java @@ -15,6 +15,7 @@ import growthbook.sdk.java.sandbox.GbCacheManager; import growthbook.sdk.java.util.DecryptionUtils; import growthbook.sdk.java.util.GrowthBookJsonUtils; +import growthbook.sdk.java.sse.SseEventPayloadValidator; import lombok.Builder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -761,13 +762,16 @@ public void onClosed(@NotNull EventSource eventSource) { @Override public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) { super.onEvent(eventSource, id, type, data); - - if (data.trim().isEmpty()) return; + if (!SseEventPayloadValidator.isValidFeaturePayload(type, data)) { + return; + } try { handler.onFeaturesResponse(data); } catch (FeatureFetchException e) { - log.error(e.getMessage(), e); + log.error("Failed to process SSE feature payload: {}", e.getMessage(), e); + } catch (RuntimeException e) { + log.error("Unexpected error while processing SSE feature payload.", e); } } diff --git a/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java b/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java index 3e8ef782..fe36c82d 100644 --- a/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java +++ b/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java @@ -4,7 +4,6 @@ import com.google.gson.JsonObject; import growthbook.sdk.java.model.Feature; import growthbook.sdk.java.multiusermode.util.TransformationUtil; -import growthbook.sdk.java.sandbox.FileCachingManagerImpl; import growthbook.sdk.java.sandbox.GbCacheManager; import growthbook.sdk.java.util.DecryptionUtils; import growthbook.sdk.java.exception.FeatureFetchException; @@ -531,7 +530,7 @@ public void run() { dataBuffer.append(line.substring(QUANTITY_TO_CUT_SSE).trim()).append("\n"); } else if (line.isEmpty()) { String data = dataBuffer.toString(); - if (!data.isEmpty()) { + if (!data.trim().isEmpty()) { onResponseJson(data, false); } dataBuffer.setLength(0); diff --git a/lib/src/main/java/growthbook/sdk/java/sse/SseEventPayloadValidator.java b/lib/src/main/java/growthbook/sdk/java/sse/SseEventPayloadValidator.java new file mode 100644 index 00000000..cb07ac4f --- /dev/null +++ b/lib/src/main/java/growthbook/sdk/java/sse/SseEventPayloadValidator.java @@ -0,0 +1,25 @@ +package growthbook.sdk.java.sse; + +import growthbook.sdk.java.util.StringUtils; +import lombok.experimental.UtilityClass; + +import javax.annotation.Nullable; +import java.util.Locale; + +@UtilityClass +public class SseEventPayloadValidator { + public boolean isValidFeaturePayload(@Nullable String eventType, @Nullable String data) { + return !StringUtils.isBlank(data) && !isHeartbeatEvent(eventType); + } + + private boolean isHeartbeatEvent(@Nullable String eventType) { + if (StringUtils.isBlank(eventType)) { + return false; + } + + String normalizedType = eventType.trim().toLowerCase(Locale.ROOT); + return "heartbeat".equals(normalizedType) + || "keepalive".equals(normalizedType) + || "ping".equals(normalizedType); + } +} diff --git a/lib/src/main/java/growthbook/sdk/java/util/StringUtils.java b/lib/src/main/java/growthbook/sdk/java/util/StringUtils.java index ba255314..99f36e28 100644 --- a/lib/src/main/java/growthbook/sdk/java/util/StringUtils.java +++ b/lib/src/main/java/growthbook/sdk/java/util/StringUtils.java @@ -1,10 +1,19 @@ package growthbook.sdk.java.util; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.stream.Collectors; +import lombok.experimental.UtilityClass; + +@UtilityClass public class StringUtils { + + public static boolean isBlank(@Nullable String value) { + return value == null || value.trim().isEmpty(); + } + public static String padLeftZeros(String inputString, Integer length) { if (inputString.length() >= length) { return inputString; diff --git a/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java b/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java index ea8603ff..63bf19fb 100644 --- a/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java +++ b/lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java @@ -1,11 +1,24 @@ package growthbook.sdk.java; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import growthbook.sdk.java.util.StringUtils; import org.junit.jupiter.api.Test; class StringUtilsTest { + @Test + void isBlankReturnsTrueForNullEmptyAndWhitespace() { + assertTrue(StringUtils.isBlank(null)); + assertTrue(StringUtils.isBlank("")); + assertTrue(StringUtils.isBlank(" ")); + } + + @Test + void isBlankReturnsFalseForNonBlankValues() { + assertFalse(StringUtils.isBlank("features")); + } @Test void padLeftZeros() { diff --git a/lib/src/test/java/growthbook/sdk/java/sse/SseEventPayloadValidatorTest.java b/lib/src/test/java/growthbook/sdk/java/sse/SseEventPayloadValidatorTest.java new file mode 100644 index 00000000..c9e50c9e --- /dev/null +++ b/lib/src/test/java/growthbook/sdk/java/sse/SseEventPayloadValidatorTest.java @@ -0,0 +1,33 @@ +package growthbook.sdk.java.sse; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SseEventPayloadValidatorTest { + + @Test + void rejectsEmptyPayloads() { + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("features", "")); + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("features", " ")); + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("features", null)); + } + + @Test + void acceptsNonEmptyPayloads() { + assertTrue(SseEventPayloadValidator.isValidFeaturePayload( + "features", + "{\"features\":{\"test\":{\"defaultValue\":true}}}" + )); + } + + @Test + void rejectsHeartbeatEvents() { + String payload = "{\"features\":{\"test\":{\"defaultValue\":true}}}"; + + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("heartbeat", payload)); + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("keepalive", payload)); + assertFalse(SseEventPayloadValidator.isValidFeaturePayload("ping", payload)); + } +} From 7d867cbb66894ecb342f16250a49bdb843f61fa8 Mon Sep 17 00:00:00 2001 From: Volodymyr Nazarkevych Date: Thu, 25 Jun 2026 12:10:48 +0300 Subject: [PATCH 2/2] fix: handle empty SSE event payloads #Apx-1 --- lib/src/main/java/growthbook/sdk/java/model/SseKey.java | 3 ++- .../sdk/java/repository/NativeJavaGbFeatureRepository.java | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/growthbook/sdk/java/model/SseKey.java b/lib/src/main/java/growthbook/sdk/java/model/SseKey.java index 09fb8a55..b5cebba8 100644 --- a/lib/src/main/java/growthbook/sdk/java/model/SseKey.java +++ b/lib/src/main/java/growthbook/sdk/java/model/SseKey.java @@ -4,7 +4,8 @@ @Getter public enum SseKey { - DATA("data:"); + DATA("data:"), + EVENT("event:"); private final String key; SseKey(String key) { diff --git a/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java b/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java index fe36c82d..75d039b5 100644 --- a/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java +++ b/lib/src/main/java/growthbook/sdk/java/repository/NativeJavaGbFeatureRepository.java @@ -15,6 +15,7 @@ import growthbook.sdk.java.model.RequestBodyForRemoteEval; import growthbook.sdk.java.model.SseKey; import growthbook.sdk.java.model.GBContext; +import growthbook.sdk.java.sse.SseEventPayloadValidator; import lombok.Builder; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -524,16 +525,20 @@ public void run() { reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; StringBuilder dataBuffer = new StringBuilder(); + String eventType = null; while ((line = reader.readLine()) != null) { if (line.startsWith(SseKey.DATA.getKey())) { dataBuffer.append(line.substring(QUANTITY_TO_CUT_SSE).trim()).append("\n"); + } else if (line.startsWith(SseKey.EVENT.getKey())) { + eventType = line.substring(SseKey.EVENT.getKey().length()).trim(); } else if (line.isEmpty()) { String data = dataBuffer.toString(); - if (!data.trim().isEmpty()) { + if (SseEventPayloadValidator.isValidFeaturePayload(eventType, data)) { onResponseJson(data, false); } dataBuffer.setLength(0); + eventType = null; } } } catch (Exception e) {