diff --git a/CHANGELOG.md b/CHANGELOG.md index a7fd53a52..d33d9ad7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly Java SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [5.10.7] - 2023-01-09 +### Fixed: +- If the stream connection failed when the SDK had only partially received a piece of JSON data from the stream, the SDK was sometimes logging a misleading error message about invalid JSON in addition to the normal error message about the connection failure. + ## [6.0.3] - 2023-01-06 ### Fixed: - Fixed unintended error behavior when the SDK is being shut down, if streaming is enabled. The symptom was that 1. the SDK could log a misleading message about a network error (in reality this was just the connection being deliberately closed) and 2. an uncaught exception could be thrown from the worker thread that managed that connection. The uncaught exception would be ignored in a default JVM configuration, but it could have more serious consequences in an application that had configured a default exception handler to be triggered by all uncaught exceptions. diff --git a/build.gradle b/build.gradle index 7f524e2db..bd4b66e61 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ ext.versions = [ "launchdarklyJavaSdkInternal": "1.0.0", "launchdarklyLogging": "1.1.0", "okhttp": "4.9.3", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource - "okhttpEventsource": "4.0.0", + "okhttpEventsource": "4.1.0", "slf4j": "1.7.21", "snakeyaml": "1.32", "jedis": "2.9.0" diff --git a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java index a54b291e7..45bb4418e 100644 --- a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java @@ -11,6 +11,7 @@ import com.launchdarkly.eventsource.MessageEvent; import com.launchdarkly.eventsource.StreamClosedByCallerException; import com.launchdarkly.eventsource.StreamClosedByServerException; +import com.launchdarkly.eventsource.StreamClosedWithIncompleteMessageException; import com.launchdarkly.eventsource.StreamEvent; import com.launchdarkly.eventsource.StreamException; import com.launchdarkly.eventsource.StreamHttpErrorException; @@ -275,6 +276,15 @@ private void handleMessage(MessageEvent event, CompletableFuture initFutur lastStoreUpdateFailed = false; dataSourceUpdates.updateStatus(State.VALID, null); } catch (StreamInputException e) { + if (exceptionHasCause(e, StreamClosedWithIncompleteMessageException.class)) { + // JSON parsing failed because the event was cut off prematurely-- because the + // stream got closed. In this case we should simply throw the event away; the + // closing of the stream will be handled separately on our next pass through + // the loop, and is logged separately. There's no point in logging an error + // about invalid JSON when the real problem is a broken connection; invalid + // JSON is significant only if we think we have a complete message. + return; + } logger.error("LaunchDarkly service request failed or received invalid data: {}", LogValues.exceptionSummary(e)); logger.debug(LogValues.exceptionTrace(e)); @@ -303,6 +313,13 @@ private void handleMessage(MessageEvent event, CompletableFuture initFutur } } + private static boolean exceptionHasCause(Throwable e, Class c) { + if (c.isAssignableFrom(e.getClass())) { + return true; + } + return e.getCause() != null && exceptionHasCause(e.getCause(), c); + } + private void handlePut(Reader eventData, CompletableFuture initFuture) throws StreamInputException, StreamStoreException { recordStreamInit(false); diff --git a/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java b/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java index cc7808f45..71ef5dec8 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvalResultTest.java @@ -1,13 +1,13 @@ package com.launchdarkly.sdk.server; -import java.util.function.Function; - import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDValue; import org.junit.Test; +import java.util.function.Function; + import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; import static com.launchdarkly.sdk.EvaluationReason.ErrorKind.WRONG_TYPE; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java index 96a8f1240..cb86d28dd 100644 --- a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java @@ -725,6 +725,26 @@ public void closingStreamProcessorDoesNotLogNetworkError() throws Exception { } } } + + @Test + public void streamFailingWithIncompleteEventDoesNotLogJsonError() throws Exception { + String incompleteEvent = "event: put\ndata: {\"flags\":"; + Handler stream1 = Handlers.all( + Handlers.SSE.start(), + Handlers.writeChunkString(incompleteEvent) + ); + Handler stream2 = streamResponse(EMPTY_DATA_EVENT); + Handler stream1Then2 = Handlers.sequential(stream1, stream2); + + try (HttpServer server = HttpServer.start(stream1Then2)) { + try (StreamProcessor sp = createStreamProcessor(null, server.getUri())) { + sp.start(); + dataSourceUpdates.awaitInit(); + + assertThat(logCapture.awaitMessage(LDLogLevel.ERROR, 0), nullValue()); + } + } + } private void testUnrecoverableHttpError(int statusCode) throws Exception { Handler errorResp = Handlers.status(statusCode);