multiProviderBuilder()
+ .errorMessage(ProviderError.buildAggregateMessage("Flag not found in any provider", collectedErrors))
.errorCode(FLAG_NOT_FOUND)
+ .providerErrors(collectedErrors)
.build();
}
}
diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java
index 6a3fc4433..2f4e6d5ba 100644
--- a/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java
+++ b/src/main/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategy.java
@@ -4,6 +4,8 @@
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.ProviderEvaluation;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.NoArgsConstructor;
@@ -12,9 +14,10 @@
/**
* First Successful Strategy.
*
- * Similar to “First Match”, except that errors from evaluated providers do not halt execution.
+ *
Similar to "First Match", except that errors from evaluated providers do not halt execution.
* Instead, it returns the first successful result from a provider. If no provider successfully
- * responds, it returns a {@code GENERAL} error result.
+ * responds, it returns a {@code GENERAL} error result that includes per-provider error details
+ * describing why each provider failed.
*/
@Slf4j
@NoArgsConstructor
@@ -27,22 +30,30 @@ public ProviderEvaluation evaluate(
T defaultValue,
EvaluationContext ctx,
Function> providerFunction) {
- for (FeatureProvider provider : providers.values()) {
+ List collectedErrors = new ArrayList<>();
+
+ for (Map.Entry entry : providers.entrySet()) {
+ String providerName = entry.getKey();
+ FeatureProvider provider = entry.getValue();
try {
ProviderEvaluation res = providerFunction.apply(provider);
if (res.getErrorCode() == null) {
// First successful result (no error code)
return res;
}
- } catch (Exception ignored) {
- // swallow and continue; errors from individual providers
- // are not fatal for this strategy
+ // Record error-coded result
+ collectedErrors.add(ProviderError.fromResult(providerName, res.getErrorCode(), res.getErrorMessage()));
+ } catch (Exception e) {
+ // Record thrown exception
+ collectedErrors.add(ProviderError.fromException(providerName, e));
}
}
- return ProviderEvaluation.builder()
- .errorMessage("No provider successfully responded")
+ return MultiProviderEvaluation.multiProviderBuilder()
+ .errorMessage(
+ ProviderError.buildAggregateMessage("No provider successfully responded", collectedErrors))
.errorCode(ErrorCode.GENERAL)
+ .providerErrors(collectedErrors)
.build();
}
}
diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderEvaluation.java b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderEvaluation.java
new file mode 100644
index 000000000..9757604aa
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/multiprovider/MultiProviderEvaluation.java
@@ -0,0 +1,123 @@
+package dev.openfeature.sdk.multiprovider;
+
+import dev.openfeature.sdk.ErrorCode;
+import dev.openfeature.sdk.ImmutableMetadata;
+import dev.openfeature.sdk.ProviderEvaluation;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A {@link ProviderEvaluation} subtype returned by multi-provider strategies that carries
+ * per-provider error details.
+ *
+ * This type can represent both successful and failed evaluations. When a strategy exhausts
+ * all providers without a successful result, the per-provider errors describe why each provider
+ * failed. Custom strategies may also use this type for successful results to surface information
+ * about providers that were skipped or failed before the successful one.
+ *
+ *
Usage:
+ *
{@code
+ * ProviderEvaluation result = strategy.evaluate(...);
+ * if (result instanceof MultiProviderEvaluation multiResult) {
+ * for (ProviderError error : multiResult.getProviderErrors()) {
+ * log.warn("Provider {} failed: {} - {}",
+ * error.getProviderName(), error.getErrorCode(), error.getErrorMessage());
+ * }
+ * }
+ * }
+ *
+ * @param the type of the flag being evaluated
+ */
+public class MultiProviderEvaluation extends ProviderEvaluation {
+
+ private final List providerErrors;
+
+ private MultiProviderEvaluation(
+ T value,
+ String variant,
+ String reason,
+ ErrorCode errorCode,
+ String errorMessage,
+ ImmutableMetadata flagMetadata,
+ List providerErrors) {
+ super(value, variant, reason, errorCode, errorMessage, flagMetadata);
+ this.providerErrors =
+ providerErrors != null ? Collections.unmodifiableList(providerErrors) : Collections.emptyList();
+ }
+
+ /**
+ * Returns the per-provider error details.
+ *
+ * Each entry describes why a specific provider failed during multi-provider evaluation.
+ *
+ * @return an unmodifiable list of per-provider errors, never {@code null}
+ */
+ public List getProviderErrors() {
+ return providerErrors;
+ }
+
+ /**
+ * Create a new builder for {@link MultiProviderEvaluation}.
+ *
+ * @param the flag value type
+ * @return a new builder
+ */
+ public static Builder multiProviderBuilder() {
+ return new Builder<>();
+ }
+
+ /**
+ * Builder for {@link MultiProviderEvaluation}.
+ *
+ * @param the flag value type
+ */
+ public static class Builder {
+ private T value;
+ private String variant;
+ private String reason;
+ private ErrorCode errorCode;
+ private String errorMessage;
+ private ImmutableMetadata flagMetadata;
+ private List providerErrors;
+
+ public Builder value(T value) {
+ this.value = value;
+ return this;
+ }
+
+ public Builder variant(String variant) {
+ this.variant = variant;
+ return this;
+ }
+
+ public Builder reason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+
+ public Builder errorCode(ErrorCode errorCode) {
+ this.errorCode = errorCode;
+ return this;
+ }
+
+ public Builder errorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ return this;
+ }
+
+ public Builder flagMetadata(ImmutableMetadata flagMetadata) {
+ this.flagMetadata = flagMetadata;
+ return this;
+ }
+
+ public Builder providerErrors(List providerErrors) {
+ this.providerErrors = providerErrors;
+ return this;
+ }
+
+ public MultiProviderEvaluation build() {
+ return new MultiProviderEvaluation<>(
+ value, variant, reason, errorCode, errorMessage, flagMetadata, providerErrors);
+ }
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/ProviderError.java b/src/main/java/dev/openfeature/sdk/multiprovider/ProviderError.java
new file mode 100644
index 000000000..921b881fa
--- /dev/null
+++ b/src/main/java/dev/openfeature/sdk/multiprovider/ProviderError.java
@@ -0,0 +1,70 @@
+package dev.openfeature.sdk.multiprovider;
+
+import dev.openfeature.sdk.ErrorCode;
+import dev.openfeature.sdk.exceptions.OpenFeatureError;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * Represents an error from a single provider during multi-provider evaluation.
+ *
+ * Captures the provider name, error code, error message, and optionally the original exception
+ * that occurred during flag evaluation. This allows callers to inspect per-provider error details
+ * when a multi-provider strategy exhausts all providers without a successful result.
+ */
+@Data
+@Builder
+@AllArgsConstructor
+public class ProviderError {
+ private String providerName;
+ private ErrorCode errorCode;
+ private String errorMessage;
+ private Exception exception;
+
+ /**
+ * Create a ProviderError from an error-coded {@code ProviderEvaluation} result.
+ *
+ * @param providerName the name of the provider that returned the error
+ * @param errorCode the error code from the evaluation result
+ * @param errorMessage the error message from the evaluation result (may be {@code null})
+ * @return a new ProviderError
+ */
+ public static ProviderError fromResult(String providerName, ErrorCode errorCode, String errorMessage) {
+ return new ProviderError(providerName, errorCode, errorMessage, null);
+ }
+
+ /**
+ * Create a ProviderError from a thrown exception.
+ *
+ * @param providerName the name of the provider that threw the exception
+ * @param exception the exception that was thrown
+ * @return a new ProviderError
+ */
+ public static ProviderError fromException(String providerName, Exception exception) {
+ ErrorCode code = ErrorCode.GENERAL;
+ if (exception instanceof OpenFeatureError) {
+ code = ((OpenFeatureError) exception).getErrorCode();
+ }
+ return new ProviderError(providerName, code, exception.getMessage(), exception);
+ }
+
+ /**
+ * Build an aggregate error message from a list of provider errors.
+ *
+ * @param baseMessage the base message to use (e.g. "No provider successfully responded")
+ * @param errors the list of per-provider errors
+ * @return an aggregate message including per-provider details
+ */
+ public static String buildAggregateMessage(String baseMessage, List errors) {
+ String details = errors.stream().map(ProviderError::toString).collect(Collectors.joining(", "));
+ return baseMessage + ". Provider errors: [" + details + "]";
+ }
+
+ @Override
+ public String toString() {
+ return providerName + ": " + errorCode + " (" + (errorMessage != null ? errorMessage : "unknown") + ")";
+ }
+}
diff --git a/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java
index 4c25fe8f0..31b769449 100644
--- a/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java
+++ b/src/main/java/dev/openfeature/sdk/multiprovider/Strategy.java
@@ -14,6 +14,10 @@
* Order or select providers
* Handle {@code FLAG_NOT_FOUND} results
* Handle errors and exceptions from providers
+ * Collect per-provider error details when no provider returns a successful result.
+ * Implementations should return a {@link MultiProviderEvaluation} populated with
+ * a {@link ProviderError} for each failed provider, so that callers can inspect individual
+ * failure reasons.
*
*/
public interface Strategy {
diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java
index 405a2f094..a7070fd11 100644
--- a/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java
+++ b/src/test/java/dev/openfeature/sdk/multiprovider/BaseStrategyTest.java
@@ -211,4 +211,17 @@ protected void setupProviderSuccess(FeatureProvider provider, String value) {
when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
.thenReturn(result);
}
+
+ protected void setupProviderErrorWithMessage(FeatureProvider provider, ErrorCode errorCode, String errorMessage) {
+ ProviderEvaluation result = mock(ProviderEvaluation.class);
+ when(result.getErrorCode()).thenReturn(errorCode);
+ when(result.getErrorMessage()).thenReturn(errorMessage);
+ when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
+ .thenReturn(result);
+ }
+
+ protected void setupProviderException(FeatureProvider provider, RuntimeException exception) {
+ when(provider.getStringEvaluation(BaseStrategyTest.FLAG_KEY, DEFAULT_STRING, null))
+ .thenThrow(exception);
+ }
}
diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategyTest.java
index 0205961cd..95a1d603e 100644
--- a/src/test/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategyTest.java
+++ b/src/test/java/dev/openfeature/sdk/multiprovider/FirstMatchStrategyTest.java
@@ -1,11 +1,15 @@
package dev.openfeature.sdk.multiprovider;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.exceptions.FlagNotFoundError;
+import java.util.List;
import org.junit.jupiter.api.Test;
class FirstMatchStrategyTest extends BaseStrategyTest {
@@ -59,19 +63,30 @@ void shouldReturnSuccessWhenFirstProviderSucceeds() {
}
@Test
- void shouldThrowFlagNotFoundWhenAllProvidersReturnFlagNotFound() {
+ void shouldReturnMultiProviderEvaluationWhenAllProvidersReturnFlagNotFound() {
setupProviderFlagNotFound(mockProvider1);
setupProviderFlagNotFound(mockProvider2);
setupProviderFlagNotFound(mockProvider3);
- ProviderEvaluation providerEvaluation = strategy.evaluate(
+ ProviderEvaluation result = strategy.evaluate(
orderedProviders,
FLAG_KEY,
DEFAULT_STRING,
null,
p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
- assertEquals(ErrorCode.FLAG_NOT_FOUND, providerEvaluation.getErrorCode());
- assertEquals("Flag not found in any provider", providerEvaluation.getErrorMessage());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, result.getErrorCode());
+ assertTrue(result.getErrorMessage().contains("Flag not found in any provider"));
+
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(3, errors.size());
+ assertEquals("provider1", errors.get(0).getProviderName());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, errors.get(0).getErrorCode());
+ assertEquals("provider2", errors.get(1).getProviderName());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, errors.get(1).getErrorCode());
+ assertEquals("provider3", errors.get(2).getProviderName());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, errors.get(2).getErrorCode());
}
@Test
@@ -88,4 +103,53 @@ void shouldSkipMultipleFlagNotFoundAndReturnFirstOtherError() {
assertNotNull(result);
assertEquals(ErrorCode.PARSE_ERROR, result.getErrorCode());
}
+
+ @Test
+ void shouldCaptureThrownFlagNotFoundErrorsAsProviderErrors() {
+ setupProviderException(mockProvider1, new FlagNotFoundError("not in provider1"));
+ setupProviderException(mockProvider2, new FlagNotFoundError("not in provider2"));
+ setupProviderException(mockProvider3, new FlagNotFoundError("not in provider3"));
+
+ ProviderEvaluation result = strategy.evaluate(
+ orderedProviders,
+ FLAG_KEY,
+ DEFAULT_STRING,
+ null,
+ p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
+
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, result.getErrorCode());
+
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(3, errors.size());
+
+ assertEquals("provider1", errors.get(0).getProviderName());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, errors.get(0).getErrorCode());
+ assertEquals("not in provider1", errors.get(0).getErrorMessage());
+ assertNotNull(errors.get(0).getException());
+
+ assertEquals("provider2", errors.get(1).getProviderName());
+ assertEquals("provider3", errors.get(2).getProviderName());
+ }
+
+ @Test
+ void shouldIncludeProviderNamesInAggregateErrorMessage() {
+ setupProviderFlagNotFound(mockProvider1);
+ setupProviderFlagNotFound(mockProvider2);
+ setupProviderFlagNotFound(mockProvider3);
+
+ ProviderEvaluation result = strategy.evaluate(
+ orderedProviders,
+ FLAG_KEY,
+ DEFAULT_STRING,
+ null,
+ p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
+
+ String message = result.getErrorMessage();
+ assertTrue(message.contains("provider1"));
+ assertTrue(message.contains("provider2"));
+ assertTrue(message.contains("provider3"));
+ assertTrue(message.contains("FLAG_NOT_FOUND"));
+ }
}
diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java
index a47af8a2f..157d9e8ce 100644
--- a/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java
+++ b/src/test/java/dev/openfeature/sdk/multiprovider/FirstSuccessfulStrategyTest.java
@@ -1,11 +1,15 @@
package dev.openfeature.sdk.multiprovider;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.exceptions.GeneralError;
+import java.util.List;
import org.junit.jupiter.api.Test;
class FirstSuccessfulStrategyTest extends BaseStrategyTest {
@@ -25,22 +29,42 @@ void shouldSkipFlagNotFoundAndReturnFirstSuccess() {
assertNotNull(result);
assertEquals("success", result.getValue());
assertNull(result.getErrorCode());
+ // Successful results should NOT be MultiProviderEvaluation
+ assertTrue(!(result instanceof MultiProviderEvaluation));
}
@Test
- void shouldThrowGeneralErrorWhenAllProvidersFail() {
- setupProviderFlagNotFound(mockProvider1);
- setupProviderError(mockProvider2, ErrorCode.PARSE_ERROR);
- setupProviderError(mockProvider3, ErrorCode.TYPE_MISMATCH);
- ProviderEvaluation providerEvaluation = strategy.evaluate(
+ void shouldReturnMultiProviderEvaluationWithProviderDetailsWhenAllProvidersFail() {
+ setupProviderErrorWithMessage(mockProvider1, ErrorCode.FLAG_NOT_FOUND, "flag missing");
+ setupProviderErrorWithMessage(mockProvider2, ErrorCode.PARSE_ERROR, "parse failed");
+ setupProviderErrorWithMessage(mockProvider3, ErrorCode.TYPE_MISMATCH, "type mismatch");
+ ProviderEvaluation result = strategy.evaluate(
orderedProviders,
FLAG_KEY,
DEFAULT_STRING,
null,
p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
- assertEquals(ErrorCode.GENERAL, providerEvaluation.getErrorCode());
- assertEquals("No provider successfully responded", providerEvaluation.getErrorMessage());
+ assertEquals(ErrorCode.GENERAL, result.getErrorCode());
+ assertNotNull(result.getErrorMessage());
+ assertTrue(result.getErrorMessage().contains("No provider successfully responded"));
+
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(3, errors.size());
+
+ assertEquals("provider1", errors.get(0).getProviderName());
+ assertEquals(ErrorCode.FLAG_NOT_FOUND, errors.get(0).getErrorCode());
+ assertEquals("flag missing", errors.get(0).getErrorMessage());
+
+ assertEquals("provider2", errors.get(1).getProviderName());
+ assertEquals(ErrorCode.PARSE_ERROR, errors.get(1).getErrorCode());
+ assertEquals("parse failed", errors.get(1).getErrorMessage());
+
+ assertEquals("provider3", errors.get(2).getProviderName());
+ assertEquals(ErrorCode.TYPE_MISMATCH, errors.get(2).getErrorCode());
+ assertEquals("type mismatch", errors.get(2).getErrorMessage());
}
@Test
@@ -49,30 +73,126 @@ void shouldSkipProvidersThatOnlyReturnErrors() {
setupProviderError(mockProvider2, ErrorCode.PROVIDER_NOT_READY);
setupProviderError(mockProvider3, ErrorCode.GENERAL);
- ProviderEvaluation providerEvaluation = strategy.evaluate(
+ ProviderEvaluation result = strategy.evaluate(
orderedProviders,
FLAG_KEY,
DEFAULT_STRING,
null,
p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
- assertEquals(ErrorCode.GENERAL, providerEvaluation.getErrorCode());
- assertEquals("No provider successfully responded", providerEvaluation.getErrorMessage());
+ assertEquals(ErrorCode.GENERAL, result.getErrorCode());
+ assertTrue(result.getErrorMessage().contains("No provider successfully responded"));
+
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(3, errors.size());
+ assertEquals(ErrorCode.INVALID_CONTEXT, errors.get(0).getErrorCode());
+ assertEquals(ErrorCode.PROVIDER_NOT_READY, errors.get(1).getErrorCode());
+ assertEquals(ErrorCode.GENERAL, errors.get(2).getErrorCode());
}
@Test
- void shouldThrowGeneralErrorForNonExistentFlag() {
+ void shouldCaptureExceptionDetailsFromThrowingProviders() {
+ setupProviderException(mockProvider1, new GeneralError("connection timeout"));
+ setupProviderErrorWithMessage(mockProvider2, ErrorCode.PARSE_ERROR, "bad json");
+ setupProviderException(mockProvider3, new RuntimeException("unexpected failure"));
+
+ ProviderEvaluation result = strategy.evaluate(
+ orderedProviders,
+ FLAG_KEY,
+ DEFAULT_STRING,
+ null,
+ p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
+
+ assertEquals(ErrorCode.GENERAL, result.getErrorCode());
+
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(3, errors.size());
+
+ // First provider threw GeneralError
+ assertEquals("provider1", errors.get(0).getProviderName());
+ assertEquals(ErrorCode.GENERAL, errors.get(0).getErrorCode());
+ assertEquals("connection timeout", errors.get(0).getErrorMessage());
+ assertNotNull(errors.get(0).getException());
+
+ // Second provider returned error-coded result
+ assertEquals("provider2", errors.get(1).getProviderName());
+ assertEquals(ErrorCode.PARSE_ERROR, errors.get(1).getErrorCode());
+ assertEquals("bad json", errors.get(1).getErrorMessage());
+ assertNull(errors.get(1).getException());
+
+ // Third provider threw RuntimeException (non-OpenFeatureError)
+ assertEquals("provider3", errors.get(2).getProviderName());
+ assertEquals(ErrorCode.GENERAL, errors.get(2).getErrorCode());
+ assertEquals("unexpected failure", errors.get(2).getErrorMessage());
+ assertNotNull(errors.get(2).getException());
+ }
+
+ @Test
+ void shouldReturnMultiProviderEvaluationForNonExistentFlag() {
orderedProviders.clear();
orderedProviders.put("old-provider", inMemoryProvider1);
orderedProviders.put("new-provider", inMemoryProvider2);
- ProviderEvaluation providerEvaluation = strategy.evaluate(
+ ProviderEvaluation result = strategy.evaluate(
+ orderedProviders,
+ FLAG_KEY,
+ DEFAULT_STRING,
+ null,
+ p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
+
+ assertEquals(ErrorCode.GENERAL, result.getErrorCode());
+ assertTrue(result.getErrorMessage().contains("No provider successfully responded"));
+
+ // InMemoryProvider throws FlagNotFoundError, which should be captured
+ MultiProviderEvaluation multiResult = assertInstanceOf(MultiProviderEvaluation.class, result);
+ List errors = multiResult.getProviderErrors();
+ assertNotNull(errors);
+ assertEquals(2, errors.size());
+ assertEquals("old-provider", errors.get(0).getProviderName());
+ assertEquals("new-provider", errors.get(1).getProviderName());
+ }
+
+ @Test
+ void shouldReturnFirstSuccessEvenAfterErrors() {
+ setupProviderError(mockProvider1, ErrorCode.PARSE_ERROR);
+ setupProviderException(mockProvider2, new GeneralError("timeout"));
+ setupProviderSuccess(mockProvider3, "finally-success");
+
+ ProviderEvaluation result = strategy.evaluate(
+ orderedProviders,
+ FLAG_KEY,
+ DEFAULT_STRING,
+ null,
+ p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
+
+ assertNotNull(result);
+ assertEquals("finally-success", result.getValue());
+ assertNull(result.getErrorCode());
+ assertTrue(!(result instanceof MultiProviderEvaluation));
+ }
+
+ @Test
+ void shouldIncludeProviderErrorDetailsInErrorMessage() {
+ setupProviderErrorWithMessage(mockProvider1, ErrorCode.PARSE_ERROR, "parse failed");
+ setupProviderErrorWithMessage(mockProvider2, ErrorCode.GENERAL, "timeout");
+ setupProviderErrorWithMessage(mockProvider3, ErrorCode.FLAG_NOT_FOUND, "not found");
+
+ ProviderEvaluation result = strategy.evaluate(
orderedProviders,
FLAG_KEY,
DEFAULT_STRING,
null,
p -> p.getStringEvaluation(FLAG_KEY, DEFAULT_STRING, null));
- assertEquals(ErrorCode.GENERAL, providerEvaluation.getErrorCode());
- assertEquals("No provider successfully responded", providerEvaluation.getErrorMessage());
+ String message = result.getErrorMessage();
+ assertTrue(message.contains("provider1"));
+ assertTrue(message.contains("PARSE_ERROR"));
+ assertTrue(message.contains("provider2"));
+ assertTrue(message.contains("GENERAL"));
+ assertTrue(message.contains("provider3"));
+ assertTrue(message.contains("FLAG_NOT_FOUND"));
}
}
diff --git a/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java b/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java
index 887b71d0a..d69cd821f 100644
--- a/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java
+++ b/src/test/java/dev/openfeature/sdk/multiprovider/MultiProviderTest.java
@@ -115,7 +115,11 @@ void shouldUseDefaultFirstMatchStrategy() {
ProviderEvaluation providerEvaluation = multiProvider.getStringEvaluation("non-existing", "", null);
assertEquals(ErrorCode.FLAG_NOT_FOUND, providerEvaluation.getErrorCode());
- assertEquals("Flag not found in any provider", providerEvaluation.getErrorMessage());
+ assertEquals(
+ "Flag not found in any provider. Provider errors: ["
+ + "old-provider: FLAG_NOT_FOUND (flag non-existing not found), "
+ + "new-provider: FLAG_NOT_FOUND (flag non-existing not found)]",
+ providerEvaluation.getErrorMessage());
}
@SneakyThrows