From dd39f2cc0ce95a7da1d51db2fbf0c5b04eaa8054 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:47:50 +0000 Subject: [PATCH 1/2] fix: Include the last data source error in the provider error event Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../openfeature/serverprovider/Provider.java | 5 ++- .../serverprovider/LifeCycleTest.java | 45 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 3031d61..ad36b66 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -210,7 +210,10 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet // Our client/provider cannot be restarted, so we just go to error. setState(ProviderState.ERROR); completer.complete(false); - emitProviderError(ProviderEventDetails.builder().message("Provider shutdown").build()); + var message = res.getLastError() != null + ? res.getLastError().getMessage() + : "the provider has encountered a permanent error or has been shutdown"; + emitProviderError(ProviderEventDetails.builder().message(message).build()); } } } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index 5f4304f..d460229 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -37,13 +37,19 @@ class DelayedDataSource implements DataSource { private Duration startDelay; private boolean willError; + private boolean errorAfterInitialization; private boolean initialized = false; private Object lock = new Object(); DataSourceUpdateSink sink; DelayedDataSource(Duration delay, boolean error, DataSourceUpdateSink sink) { + this(delay, error, false, sink); + } + + DelayedDataSource(Duration delay, boolean error, boolean errorAfterInitialization, DataSourceUpdateSink sink) { startDelay = delay; willError = error; + this.errorAfterInitialization = errorAfterInitialization; this.sink = sink; } @@ -58,6 +64,14 @@ public void run() { synchronized (lock) { initialized = true; } + if (errorAfterInitialization) { + sink.updateStatus(DataSourceStatusProvider.State.OFF, + new DataSourceStatusProvider.ErrorInfo( + DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, + 404, + "bad", + LocalDateTime.now().toInstant(ZoneOffset.UTC))); + } } else { sink.updateStatus(DataSourceStatusProvider.State.OFF, new DataSourceStatusProvider.ErrorInfo( @@ -86,15 +100,25 @@ public void close() throws IOException { class DelayedDataSourceFactory implements ComponentConfigurer { private Duration startDelay; private boolean willError; + private boolean errorAfterInitialization; DelayedDataSourceFactory(Duration delay, boolean error) { + this(delay, error, false); + } + + DelayedDataSourceFactory(Duration delay, boolean error, boolean errorAfterInitialization) { startDelay = delay; willError = error; + this.errorAfterInitialization = errorAfterInitialization; } @Override public DataSource build(ClientContext clientContext) { - return new DelayedDataSource(startDelay, willError, clientContext.getDataSourceUpdateSink()); + return new DelayedDataSource( + startDelay, + willError, + errorAfterInitialization, + clientContext.getDataSourceUpdateSink()); } } @@ -221,4 +245,23 @@ public void itCanHandleClientThatIsNotInitializedImmediatelyAndErrors() throws E assertTrue(gotErrorEvent.get(1000, TimeUnit.MILLISECONDS)); } + + @Test + public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception { + var config = new LDConfig.Builder() + .startWait(Duration.ZERO) + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config); + CompletableFuture errorMessage = new CompletableFuture<>(); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> { + errorMessage.complete(detail.getMessage()); + }); + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + assertEquals("bad", errorMessage.get(1000, TimeUnit.MILLISECONDS)); + } } From 8fb4c4adcee1b46290f2ae339c0b9a059747deb4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:10:50 +0000 Subject: [PATCH 2/2] fix: Include data source error details in provider events Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../openfeature/serverprovider/Provider.java | 2 +- .../serverprovider/LifeCycleTest.java | 66 +++++++++++++++---- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index ad36b66..b54d895 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -211,7 +211,7 @@ private void handleDataSourceStatus(DataSourceStatusProvider.Status res, Complet setState(ProviderState.ERROR); completer.complete(false); var message = res.getLastError() != null - ? res.getLastError().getMessage() + ? res.getLastError().toString() : "the provider has encountered a permanent error or has been shutdown"; emitProviderError(ProviderEventDetails.builder().message(message).build()); } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java index d460229..fafd372 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LifeCycleTest.java @@ -38,18 +38,25 @@ class DelayedDataSource implements DataSource { private Duration startDelay; private boolean willError; private boolean errorAfterInitialization; + private boolean useHttpError; private boolean initialized = false; private Object lock = new Object(); DataSourceUpdateSink sink; DelayedDataSource(Duration delay, boolean error, DataSourceUpdateSink sink) { - this(delay, error, false, sink); + this(delay, error, false, false, sink); } DelayedDataSource(Duration delay, boolean error, boolean errorAfterInitialization, DataSourceUpdateSink sink) { + this(delay, error, errorAfterInitialization, false, sink); + } + + DelayedDataSource(Duration delay, boolean error, boolean errorAfterInitialization, boolean useHttpError, + DataSourceUpdateSink sink) { startDelay = delay; willError = error; this.errorAfterInitialization = errorAfterInitialization; + this.useHttpError = useHttpError; this.sink = sink; } @@ -65,20 +72,10 @@ public void run() { initialized = true; } if (errorAfterInitialization) { - sink.updateStatus(DataSourceStatusProvider.State.OFF, - new DataSourceStatusProvider.ErrorInfo( - DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, - 404, - "bad", - LocalDateTime.now().toInstant(ZoneOffset.UTC))); + sink.updateStatus(DataSourceStatusProvider.State.OFF, errorInfo()); } } else { - sink.updateStatus(DataSourceStatusProvider.State.OFF, - new DataSourceStatusProvider.ErrorInfo( - DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, - 404, - "bad", - LocalDateTime.now().toInstant(ZoneOffset.UTC))); + sink.updateStatus(DataSourceStatusProvider.State.OFF, errorInfo()); } future.complete(null); } @@ -87,6 +84,16 @@ public void run() { return future; } + private DataSourceStatusProvider.ErrorInfo errorInfo() { + return useHttpError + ? DataSourceStatusProvider.ErrorInfo.fromHttpError(401) + : new DataSourceStatusProvider.ErrorInfo( + DataSourceStatusProvider.ErrorKind.NETWORK_ERROR, + 404, + "bad", + LocalDateTime.now().toInstant(ZoneOffset.UTC)); + } + public boolean isInitialized() { synchronized (lock) { return initialized; @@ -101,15 +108,21 @@ class DelayedDataSourceFactory implements ComponentConfigurer { private Duration startDelay; private boolean willError; private boolean errorAfterInitialization; + private boolean useHttpError; DelayedDataSourceFactory(Duration delay, boolean error) { this(delay, error, false); } DelayedDataSourceFactory(Duration delay, boolean error, boolean errorAfterInitialization) { + this(delay, error, errorAfterInitialization, false); + } + + DelayedDataSourceFactory(Duration delay, boolean error, boolean errorAfterInitialization, boolean useHttpError) { startDelay = delay; willError = error; this.errorAfterInitialization = errorAfterInitialization; + this.useHttpError = useHttpError; } @Override @@ -118,6 +131,7 @@ public DataSource build(ClientContext clientContext) { startDelay, willError, errorAfterInitialization, + useHttpError, clientContext.getDataSourceUpdateSink()); } } @@ -262,6 +276,30 @@ public void itIncludesTheDataSourceErrorInErrorEvents() throws Exception { OpenFeatureAPI.getInstance().setProviderAndWait(provider); - assertEquals("bad", errorMessage.get(1000, TimeUnit.MILLISECONDS)); + var message = errorMessage.get(1000, TimeUnit.MILLISECONDS); + assertTrue(message.contains("404")); + assertTrue(message.contains("bad")); + } + + @Test + public void itIncludesHttpDataSourceErrorInErrorEvents() throws Exception { + var config = new LDConfig.Builder() + .startWait(Duration.ZERO) + .dataSource(new DelayedDataSourceFactory(Duration.ofMillis(100), false, true, true)) + .events(Components.noEvents()) + .build(); + var provider = new Provider("fake-key", config); + CompletableFuture errorMessage = new CompletableFuture<>(); + + OpenFeatureAPI.getInstance().on(ProviderEvent.PROVIDER_ERROR, (detail) -> { + errorMessage.complete(detail.getMessage()); + }); + + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + + var message = errorMessage.get(1000, TimeUnit.MILLISECONDS); + assertNotNull(message); + assertTrue(!message.isEmpty()); + assertTrue(message.contains("401")); } }