Skip to content

[Multi-provider] FirstSuccessfulStrategy silently discards per-provider errors, returns generic message on all-fail #1900

@suthar26

Description

@suthar26

Context

Related to #1882 (Gaps identified relative to js-sdk reference implementation).

When using FirstSuccessfulStrategy with MultiProvider, if all providers fail, the user receives only a generic error:

errorCode: GENERAL
errorMessage: "No provider successfully responded"

There is no information about which providers failed, why they failed, or what error codes each provider returned. Exceptions thrown by individual providers are caught and silently swallowed. Error-coded results from providers are discarded without any record.

This makes debugging multi-provider setups very difficult, especially in production where the root cause may differ across providers (e.g., one provider has a connection timeout, another has a parse error, a third returns flag-not-found).

FirstMatchStrategy has the same issue in its all-FLAG_NOT_FOUND fallback path, though it is less impactful since it only skips FLAG_NOT_FOUND errors.

Current Behavior

FirstSuccessfulStrategy

FirstSuccessfulStrategy.java:30-41:

for (FeatureProvider provider : providers.values()) {
    try {
        ProviderEvaluation<T> res = providerFunction.apply(provider);
        if (res.getErrorCode() == null) {
            return res;
        }
        // error-coded result is silently discarded here
    } catch (Exception ignored) {
        // thrown exception is silently swallowed here
    }
}

return ProviderEvaluation.<T>builder()
        .errorMessage("No provider successfully responded")
        .errorCode(ErrorCode.GENERAL)
        .build();

Both paths (error-coded results and thrown exceptions) are discarded with no tracking. The final fallback result carries no per-provider information.

FirstMatchStrategy

FirstMatchStrategy.java:39-61 -- similarly discards individual FLAG_NOT_FOUND errors without collecting them:

return ProviderEvaluation.<T>builder()
        .errorMessage("Flag not found in any provider")
        .errorCode(FLAG_NOT_FOUND)
        .build();

Expected Behavior (js-sdk reference)

The js-sdk reference implementation handles this correctly:

  1. Every provider error is captured -- both thrown exceptions (stored as { thrownError }) and error-coded results are tracked in a resolutions[] array during iteration.

  2. Errors are aggregated on all-fail -- When all providers fail, collectProviderErrors() iterates all resolutions and builds an array of { providerName, error } for each failed provider.

  3. User receives an AggregateError -- The MultiProvider throws an AggregateError that includes:

    • .message -- contains the first error's provider name and message for convenience
    • .originalErrors[] -- array of { source: providerName, error } for every failed provider

Example of what a js-sdk user sees when 3 providers fail:

AggregateError: Provider errors occurred: ProviderA: Connection timeout
  originalErrors: [
    { source: "ProviderA", error: ConnectionTimeoutError },
    { source: "ProviderB", error: ParseError },
    { source: "ProviderC", error: FlagNotFoundError }
  ]

Versus what a java-sdk user sees for the same scenario:

errorCode: GENERAL
errorMessage: "No provider successfully responded"

Proposed Solution

  1. Collect per-provider errors during iteration -- Both FirstSuccessfulStrategy and FirstMatchStrategy should accumulate error details (provider name, error code, error message, original exception) as they iterate through providers.

  2. Expose per-provider errors in the result -- Add a List<ProviderError> field to ProviderEvaluation (and propagate through to FlagEvaluationDetails) so that on an all-fail result, the user can programmatically inspect each provider's error. A ProviderError type would carry:

    • providerName (String)
    • errorCode (ErrorCode)
    • errorMessage (String)
    • exception (Exception, nullable)
  3. Build a descriptive aggregate error message -- The errorMessage on the final ProviderEvaluation should summarize the per-provider errors, e.g.:

    No provider successfully responded. Provider errors: [provider1: PARSE_ERROR (parse failed), provider2: GENERAL (connection timeout)]
    
  4. Ensure the Strategy interface contract is backwards-compatible -- The Strategy interface itself doesn't change. The ProviderEvaluation field would be additive (defaults to an empty list).

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions