Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
],
]

_HTTP_AUTH_ERRORS: dict[int, str] = {401: "unauthorized", 403: "forbidden"}


class OFREPProvider(AbstractProvider):
def __init__(
Expand Down Expand Up @@ -176,24 +178,19 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn:
if response is None:
raise GeneralError(str(exception)) from exception

if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
self.retry_after = _parse_retry_after(retry_after)
raise GeneralError(
f"Rate limited, retry after: {retry_after}"
) from exception

self._raise_for_http_status(response, exception)
# Fallthrough: parse JSON and raise based on error code
try:
data = response.json()
except JSONDecodeError:
raise ParseError(str(exception)) from exception

error_code = ErrorCode(data["errorCode"])
try:
error_code = ErrorCode(data["errorCode"])
except ValueError:
error_code = ErrorCode.GENERAL
error_details = data["errorDetails"]

if response.status_code == 404:
raise FlagNotFoundError(error_details) from exception

if error_code == ErrorCode.PARSE_ERROR:
raise ParseError(error_details) from exception
if error_code == ErrorCode.TARGETING_KEY_MISSING:
Expand All @@ -205,6 +202,32 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn:

raise OpenFeatureError(error_code, error_details) from exception

def _raise_for_http_status(
self,
response: requests.Response,
exception: requests.RequestException,
) -> None:
status = response.status_code

if status == 429:
retry_after = response.headers.get("Retry-After")
self.retry_after = _parse_retry_after(retry_after)
raise GeneralError(
f"Rate limited, retry after: {retry_after}"
) from exception
elif status in _HTTP_AUTH_ERRORS:
raise OpenFeatureError(
ErrorCode.GENERAL, _HTTP_AUTH_ERRORS[status]
) from exception
elif status == 404:
try:
error_details = response.json()["errorDetails"]
except (JSONDecodeError, KeyError):
error_details = response.text
raise FlagNotFoundError(error_details) from exception
elif status > 400:
raise OpenFeatureError(ErrorCode.GENERAL, response.text) from exception


def _build_request_data(
evaluation_context: Optional[EvaluationContext],
Expand Down
57 changes: 57 additions & 0 deletions providers/openfeature-provider-ofrep/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from openfeature.contrib.provider.ofrep import OFREPProvider
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import (
ErrorCode,
FlagNotFoundError,
GeneralError,
InvalidContextError,
OpenFeatureError,
ParseError,
TypeMismatchError,
)
Expand Down Expand Up @@ -83,6 +85,61 @@ def test_provider_flag_not_found(ofrep_provider, requests_mock):
ofrep_provider.resolve_boolean_details("flag_key", False)


def test_provider_unauthorized(ofrep_provider, requests_mock):
requests_mock.post(
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
status_code=401,
text="unauthorized",
)

with pytest.raises(OpenFeatureError) as exc_info:
ofrep_provider.resolve_boolean_details("flag_key", False)

assert exc_info.value.error_code == ErrorCode.GENERAL
assert exc_info.value.error_message == "unauthorized"


def test_provider_forbidden(ofrep_provider, requests_mock):
requests_mock.post(
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
status_code=403,
text="forbidden",
)

with pytest.raises(OpenFeatureError) as exc_info:
ofrep_provider.resolve_boolean_details("flag_key", False)

assert exc_info.value.error_code == ErrorCode.GENERAL
assert exc_info.value.error_message == "forbidden"


def test_provider_flag_not_found_invalid_json(ofrep_provider, requests_mock):
"""Test 404 with non-JSON response falls back to response text for error details"""
requests_mock.post(
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
status_code=404,
text="Flag not found - plain text response",
)

with pytest.raises(FlagNotFoundError, match="Flag not found - plain text response"):
ofrep_provider.resolve_boolean_details("flag_key", False)


def test_provider_server_error(ofrep_provider, requests_mock):
"""Test generic OpenFeatureError for status codes > 400 (e.g. 500, 502)"""
requests_mock.post(
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
status_code=500,
text="Internal Server Error",
)

with pytest.raises(OpenFeatureError) as exc_info:
ofrep_provider.resolve_boolean_details("flag_key", False)

assert exc_info.value.error_code == ErrorCode.GENERAL
assert exc_info.value.error_message == "Internal Server Error"


def test_provider_invalid_context(ofrep_provider, requests_mock):
requests_mock.post(
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
Expand Down