diff --git a/providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py b/providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py index 69372e12..e660d341 100644 --- a/providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py +++ b/providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py @@ -42,6 +42,8 @@ ], ] +_HTTP_AUTH_ERRORS: dict[int, str] = {401: "unauthorized", 403: "forbidden"} + class OFREPProvider(AbstractProvider): def __init__( @@ -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: @@ -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], diff --git a/providers/openfeature-provider-ofrep/tests/test_provider.py b/providers/openfeature-provider-ofrep/tests/test_provider.py index a84b369d..6709faf4 100644 --- a/providers/openfeature-provider-ofrep/tests/test_provider.py +++ b/providers/openfeature-provider-ofrep/tests/test_provider.py @@ -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, ) @@ -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",