open-feature / open-feature/go-sdk
[BUG] multiprovider: NOT_READY and FATAL are absent from the state precedence table, so they resolve to READY
- Dominant language
- Go
- Stars
- 250
- Forks
- 62
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 25
Description
## Observed behavior
The state precedence tables in `openfeature/multi/multiprovider.go` define only three states:
https://github.com/open-feature/go-sdk/blob/6b0824c6c7b9b69e40f26aedcc4406ad4d3a5cec/openfeature/multi/multiprovider.go#L119-L130
`of.NotReadyState` and `of.FatalState` are missing. `evaluateState()` does a bare `stateValues[s]` lookup, so a miss yields the zero value `0`, and `stateTable[0]` maps back to **`ReadyState`**. The two highest-precedence states in Appendix A become the lowest:
https://github.com/open-feature/go-sdk/blob/6b0824c6c7b9b69e40f26aedcc4406ad4d3a5cec/openfeature/multi/multiprovider.go#L529-L540
This is reachable on the ordinary startup path: `InitWithContext` explicitly sets every inner provider to `NotReadyState` before initializing it, which immediately flips the overall status to READY.
```
status BEFORE Init = "NOT_READY"
status DURING Init (inner NOT_READY) = "READY" <- want NOT_READY
status AFTER Init = "READY"
```
Separately, `FatalState` is **unrepresentable** in this package. `eventTypeToState` maps `of.ProviderError` → `of.ErrorState` unconditionally and ignores `ProviderEventDetails.ErrorCode`, unlike core's `statesMap`, which checks for `ProviderFatalCode`:
https://github.com/open-feature/go-sdk/blob/6b0824c6c7b9b69e40f26aedcc4406ad4d3a5cec/openfeature/openfeature_api.go#L518-L528
```
multi Status() after inner PROVIDER_ERROR{PROVIDER_FATAL} = "ERROR" <- want FATAL
```
## Expected Behavior
[Appendix A](https://github.com/open-feature/spec/blob/main/specification/appendix-a-included-utilities.md#multi-provider-status) ("Multi-Provider Status") defines the precedence order as **FATAL > NOT_READY > ERROR > STALE > READY**. Both states should be present in the tables, and `eventTypeToState` should map a `PROVIDER_ERROR` carrying error code `PROVIDER_FATAL` to `FatalState`, matching core's behaviour.
This matters beyond status reporting. Appendix A: *"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."* Since `Client.evaluate` short-circuits only on `NotReadyState`/`FatalState`, this causes the client to evaluate against a fatal or not-yet-initialized multi-provider instead of returning the default value.
Note that fixing the maps alone is insufficient — `evaluateState` maps precedence back through a fixed `[3]of.State` array, so the array and both maps need to grow together.
## Steps to reproduce
```go
// slowInitProvider.Init sleeps for `delay`, then returns nil
slow := &slowInitProvider{delay: 700 * time.Millisecond}
mp, _ := multi.NewProvider(multi.StrategyFirstMatch, multi.WithProvider("slow", slow))
go mp.InitWithContext(context.Background(), of.EvaluationContext{})
time.Sleep(200 * time.Millisecond)
fmt.Println(mp.Status()) // "READY" while the only inner provider is still NOT_READY
```
For the FATAL half, have an inner provider emit `of.Event{EventType: of.ProviderError, ProviderEventDetails: of.ProviderEventDetails{ErrorCode: of.ProviderFatalCode}}` and observe `mp.Status()` report `ERROR` rather than `FATAL`.
Contributor guide
Research direction
Start in openfeature/multi/multiprovider.go, reading the state precedence tables, evaluateState, eventTypeToState, and InitWithContext. Reproduce the slow-initialization and PROVIDER_FATAL scenarios described in the issue, then verify that the status precedence is FATAL > NOT_READY > ERROR > STALE > READY and that both cases cause the expected client behavior.
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
- 74/100