Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/src/main/java/growthbook/sdk/java/model/SseKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

@Getter
public enum SseKey {
DATA("data:");
DATA("data:"),
EVENT("event:");
private final String key;

SseKey(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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;
Expand Down Expand Up @@ -1014,6 +1015,11 @@ 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);
// Heartbeat/keepalive events carry no feature changes; ignore them so they neither
// trigger a refresh nor get parsed as a feature payload.
if (SseEventPayloadValidator.isHeartbeatEvent(type)) {
return;
}

try {
if (data.trim().isEmpty()) {
Expand All @@ -1022,7 +1028,9 @@ public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Null
}
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 growthbook.sdk.java.remoteeval.RemoteEvalEndpoints;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -685,16 +686,20 @@ private void initializeSSE(Boolean retryOnFailure) {
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.isEmpty()) {
if (SseEventPayloadValidator.isValidFeaturePayload(eventType, data)) {
onResponseJson(data, false);
}
dataBuffer.setLength(0);
eventType = null;
}
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

public 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);
}
}
9 changes: 9 additions & 0 deletions lib/src/main/java/growthbook/sdk/java/util/StringUtils.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
13 changes: 13 additions & 0 deletions lib/src/test/java/growthbook/sdk/java/StringUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading