Skip to content

Commit c1d5146

Browse files
committed
fix: run error hooks if provider returns FlagResolutionDetails with non-empty error_code
1 parent 3f336b3 commit c1d5146

File tree

4 files changed

+53
-3
lines changed

4 files changed

+53
-3
lines changed

openfeature/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ def _create_provider_evaluation(
425425
raise GeneralError(error_message="Unknown flag type")
426426

427427
resolution = get_details_callable(*args)
428+
resolution.raise_for_error()
428429

429430
# we need to check the get_args to be compatible with union types.
430431
_typecheck_flag_value(resolution.value, flag_type)

openfeature/exception.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import typing
24
from enum import Enum
35

@@ -12,6 +14,24 @@ class ErrorCode(Enum):
1214
INVALID_CONTEXT = "INVALID_CONTEXT"
1315
GENERAL = "GENERAL"
1416

17+
@classmethod
18+
def to_exception(
19+
cls, error_code: ErrorCode, error_message: str
20+
) -> OpenFeatureError:
21+
return typing.cast(
22+
OpenFeatureError,
23+
{
24+
ErrorCode.PROVIDER_NOT_READY: ProviderNotReadyError,
25+
ErrorCode.PROVIDER_FATAL: ProviderFatalError,
26+
ErrorCode.FLAG_NOT_FOUND: FlagNotFoundError,
27+
ErrorCode.PARSE_ERROR: ParseError,
28+
ErrorCode.TYPE_MISMATCH: TypeMismatchError,
29+
ErrorCode.TARGETING_KEY_MISSING: TargetingKeyMissingError,
30+
ErrorCode.INVALID_CONTEXT: InvalidContextError,
31+
ErrorCode.GENERAL: GeneralError,
32+
}.get(error_code, GeneralError)(error_message),
33+
)
34+
1535

1636
class OpenFeatureError(Exception):
1737
"""

openfeature/flag_evaluation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,8 @@ class FlagResolutionDetails(typing.Generic[U_co]):
6363
reason: typing.Optional[typing.Union[str, Reason]] = None
6464
variant: typing.Optional[str] = None
6565
flag_metadata: FlagMetadata = field(default_factory=dict)
66+
67+
def raise_for_error(self) -> None:
68+
if self.error_code:
69+
raise ErrorCode.to_exception(self.error_code, self.error_message or "")
70+
return None

tests/test_client.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
import pytest
44

5-
from openfeature.api import add_hooks, clear_hooks, set_provider
5+
from openfeature.api import add_hooks, clear_hooks, get_client, set_provider
66
from openfeature.client import OpenFeatureClient
77
from openfeature.exception import ErrorCode, OpenFeatureError
8-
from openfeature.flag_evaluation import Reason
8+
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
99
from openfeature.hook import Hook
10-
from openfeature.provider import ProviderStatus
10+
from openfeature.provider import FeatureProvider, ProviderStatus
1111
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
1212
from openfeature.provider.no_op_provider import NoOpProvider
1313

@@ -236,3 +236,27 @@ def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state(
236236
assert flag_details.reason == Reason.ERROR
237237
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
238238
spy_hook.error.assert_called_once()
239+
240+
241+
def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code():
242+
# Given
243+
spy_hook = MagicMock(spec=Hook)
244+
provider = MagicMock(spec=FeatureProvider)
245+
provider.get_provider_hooks.return_value = []
246+
provider.resolve_boolean_details.return_value = FlagResolutionDetails(
247+
value=True,
248+
reason=Reason.ERROR,
249+
error_code=ErrorCode.PROVIDER_FATAL,
250+
error_message="This is an error message",
251+
)
252+
set_provider(provider)
253+
client = get_client()
254+
client.add_hooks([spy_hook])
255+
# When
256+
flag_details = client.get_boolean_details(flag_key="Key", default_value=True)
257+
# Then
258+
assert flag_details is not None
259+
assert flag_details.value
260+
assert flag_details.reason == Reason.ERROR
261+
assert flag_details.error_code == ErrorCode.PROVIDER_FATAL
262+
spy_hook.error.assert_called_once()

0 commit comments

Comments
 (0)