open-feature / open-feature/go-sdk
[BUG] a PROVIDER_CONFIGURATION_CHANGED event clears FATAL and silently resumes evaluation
- Dominant language
- Go
- Stars
- 250
- Forks
- 62
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 25
Description
## Observed behavior
A `PROVIDER_CONFIGURATION_CHANGED` event unconditionally moves a provider out of `FATAL` and back to `READY`, silently re-enabling flag evaluation against a provider that is fatally broken.
`statesMap` maps the event to `ReadyState` with no regard for the current state:
https://github.com/open-feature/go-sdk/blob/5d64ff9c5b4f362bc26cb8732d589155e9c6e2bd/openfeature/openfeature_api.go#L518-L528
and `triggerEvent` stores that state unconditionally:
https://github.com/open-feature/go-sdk/blob/5d64ff9c5b4f362bc26cb8732d589155e9c6e2bd/openfeature/event_executor.go#L316-L330
This is reachable with a single provider, but the multi-provider makes it considerably more likely: the event that clears the FATAL can come from a *different, perfectly healthy* provider, with no recovery whatsoever on the fatal one.
Running two providers behind a multi-provider, where the fake provider deliberately resolves to a non-default value so the last column distinguishes "evaluation skipped" from "provider consulted":
```
initial: client.State() = "READY" mp.Status() = "READY"
after A emits FATAL: client.State() = "FATAL" mp.Status() = "FATAL" eval -> default (skipped)
after B emits CONFIG_CHANGED: client.State() = "READY" mp.Status() = "FATAL" eval -> provider consulted
```
The multi-provider's own `Status()` still reports FATAL, so the SDK-level state and the provider's state disagree, and evaluations that should return the default silently start hitting the broken provider again.
## Expected Behavior
[Appendix A](https://github.com/open-feature/spec/blob/main/specification/appendix-a-included-utilities.md#multi-provider-status): *"The only statuses which affect evaluation behavior at the SDK client level are FATAL and NOT_READY. If a provider is in either of these states, evaluation will be skipped by the client and the default value will be returned."*
FATAL is terminal. A configuration-change event should not be able to clear it: the provider should stay FATAL and evaluation should keep short-circuiting to the default until the provider is re-initialized. `ProviderReady` is the event that legitimately signals recovery.
## Steps to reproduce
Two providers behind a multi-provider, both implementing `EventChannel()`:
```go
mp, _ := multi.NewProvider(multi.StrategyFirstMatch,
multi.WithProvider("a", a), multi.WithProvider("b", b))
_ = of.SetProviderAndWait(mp)
client := of.NewClient("domain")
// A goes fatal -> client is FATAL, evaluation correctly returns the default
a.ch <- of.Event{EventType: of.ProviderError, ProviderEventDetails: of.ProviderEventDetails{
ErrorCode: of.ProviderFatalCode, EventMetadata: map[string]any{},
}}
// healthy B emits a routine config change -> client flips to READY
b.ch <- of.Event{EventType: of.ProviderConfigChange, ProviderEventDetails: of.ProviderEventDetails{
EventMetadata: map[string]any{},
}}
fmt.Println(client.State(), mp.Status()) // "READY" "FATAL"
// client.Boolean(...) now resolves through the fatally-broken provider
```
Noticed while reviewing #580, which makes FATAL reachable in the multi-provider for the first time and so makes this observable there.
*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: 2.1.236.*
Contributor guide
Research direction
Start with the statesMap handling in openfeature/openfeature_api.go and the unconditional state storage in openfeature/event_executor.go. Trace how ProviderConfigChange and ProviderReady affect client state and evaluation, then add regression coverage showing that configuration changes do not clear FATAL while recovery still works through ProviderReady.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100