From 539204fee16c527e7cc0c6cbd76d6a259e90cbc0 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Fri, 20 Feb 2026 17:30:08 +0100 Subject: [PATCH 1/5] fix(ofrep): handle 401, 403, 404 and 5xx errors explicitly and add tests Signed-off-by: Thomas Poignant --- .../contrib/provider/ofrep/__init__.py | 20 ++++++- .../tests/test_provider.py | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) 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..92841b39 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 @@ -183,6 +183,23 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: f"Rate limited, retry after: {retry_after}" ) from exception + if response.status_code == 401: + raise OpenFeatureError(ErrorCode.GENERAL, "unauthorized") from exception + + if response.status_code == 403: + raise OpenFeatureError(ErrorCode.GENERAL, "forbidden") from exception + + if response.status_code == 404: + try: + data = response.json() + error_details = data["errorDetails"] + except JSONDecodeError: + error_details = response.text + raise FlagNotFoundError(error_details) from exception + + if response.status_code > 400: + raise OpenFeatureError(ErrorCode.GENERAL, response.text) from exception + try: data = response.json() except JSONDecodeError: @@ -191,9 +208,6 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: error_code = ErrorCode(data["errorCode"]) 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: 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", From 45f0835b3873eedb4b04ab1713986a2627afb3c2 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Fri, 20 Feb 2026 17:32:42 +0100 Subject: [PATCH 2/5] refactor(ofrep): extract _raise_for_http_status and _raise_for_error_code to satisfy ruff complexity Signed-off-by: Thomas Poignant --- .../contrib/provider/ofrep/__init__.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) 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 92841b39..52928c06 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 @@ -176,6 +176,20 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: if response is None: raise GeneralError(str(exception)) 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 + + self._raise_for_error_code(data, exception) + + def _raise_for_http_status( + self, + response: requests.Response, + exception: requests.RequestException, + ) -> None: if response.status_code == 429: retry_after = response.headers.get("Retry-After") self.retry_after = _parse_retry_after(retry_after) @@ -200,11 +214,11 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: if response.status_code > 400: raise OpenFeatureError(ErrorCode.GENERAL, response.text) from exception - try: - data = response.json() - except JSONDecodeError: - raise ParseError(str(exception)) from exception - + def _raise_for_error_code( + self, + data: dict[str, Any], + exception: requests.RequestException, + ) -> NoReturn: error_code = ErrorCode(data["errorCode"]) error_details = data["errorDetails"] From 35fb9bdd3eba4de5b406bbfe3aeea305f16c2292 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Fri, 20 Feb 2026 17:36:12 +0100 Subject: [PATCH 3/5] refactor(ofrep): use elif chain and _HTTP_AUTH_ERRORS dict in _raise_for_http_status Signed-off-by: Thomas Poignant --- .../contrib/provider/ofrep/__init__.py | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) 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 52928c06..97707dfd 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__( @@ -183,56 +185,46 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: except JSONDecodeError: raise ParseError(str(exception)) from exception - self._raise_for_error_code(data, exception) + error_code = ErrorCode(data["errorCode"]) + error_details = data["errorDetails"] + + if error_code == ErrorCode.PARSE_ERROR: + raise ParseError(error_details) from exception + if error_code == ErrorCode.TARGETING_KEY_MISSING: + raise TargetingKeyMissingError(error_details) from exception + if error_code == ErrorCode.INVALID_CONTEXT: + raise InvalidContextError(error_details) from exception + if error_code == ErrorCode.GENERAL: + raise GeneralError(error_details) from exception + + raise OpenFeatureError(error_code, error_details) from exception def _raise_for_http_status( self, response: requests.Response, exception: requests.RequestException, ) -> None: - if response.status_code == 429: + 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 - - if response.status_code == 401: - raise OpenFeatureError(ErrorCode.GENERAL, "unauthorized") from exception - - if response.status_code == 403: - raise OpenFeatureError(ErrorCode.GENERAL, "forbidden") from exception - - if response.status_code == 404: + elif status in _HTTP_AUTH_ERRORS: + raise OpenFeatureError( + ErrorCode.GENERAL, _HTTP_AUTH_ERRORS[status] + ) from exception + elif status == 404: try: - data = response.json() - error_details = data["errorDetails"] - except JSONDecodeError: + error_details = response.json()["errorDetails"] + except (JSONDecodeError, KeyError): error_details = response.text raise FlagNotFoundError(error_details) from exception - - if response.status_code > 400: + elif status > 400: raise OpenFeatureError(ErrorCode.GENERAL, response.text) from exception - def _raise_for_error_code( - self, - data: dict[str, Any], - exception: requests.RequestException, - ) -> NoReturn: - error_code = ErrorCode(data["errorCode"]) - error_details = data["errorDetails"] - - if error_code == ErrorCode.PARSE_ERROR: - raise ParseError(error_details) from exception - if error_code == ErrorCode.TARGETING_KEY_MISSING: - raise TargetingKeyMissingError(error_details) from exception - if error_code == ErrorCode.INVALID_CONTEXT: - raise InvalidContextError(error_details) from exception - if error_code == ErrorCode.GENERAL: - raise GeneralError(error_details) from exception - - raise OpenFeatureError(error_code, error_details) from exception - def _build_request_data( evaluation_context: Optional[EvaluationContext], From fd48f5897b654b74939caeebc943516b409efff4 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Fri, 20 Feb 2026 17:58:28 +0100 Subject: [PATCH 4/5] fix(ofrep): use data.get for errorCode fallback to GENERAL Signed-off-by: Thomas Poignant --- .../src/openfeature/contrib/provider/ofrep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 97707dfd..f60de396 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 @@ -185,7 +185,7 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: except JSONDecodeError: raise ParseError(str(exception)) from exception - error_code = ErrorCode(data["errorCode"]) + error_code = ErrorCode(data.get("errorCode", "GENERAL")) error_details = data["errorDetails"] if error_code == ErrorCode.PARSE_ERROR: From 4af0a1182a43778ecee92b554bbd370987fc7337 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Fri, 20 Feb 2026 17:59:42 +0100 Subject: [PATCH 5/5] fix(ofrep): handle invalid errorCode with try/except, fallback to GENERAL Signed-off-by: Thomas Poignant --- .../src/openfeature/contrib/provider/ofrep/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 f60de396..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 @@ -185,7 +185,10 @@ def _handle_error(self, exception: requests.RequestException) -> NoReturn: except JSONDecodeError: raise ParseError(str(exception)) from exception - error_code = ErrorCode(data.get("errorCode", "GENERAL")) + try: + error_code = ErrorCode(data["errorCode"]) + except ValueError: + error_code = ErrorCode.GENERAL error_details = data["errorDetails"] if error_code == ErrorCode.PARSE_ERROR: