From 388d7ab38b421183357abef23875c9a8dc2bee2b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 7 Apr 2026 23:50:51 +0000 Subject: [PATCH 1/2] Fix seer RPC exception handling to preserve HTTP status codes Previously, all exceptions from downstream services (Sentry/Snuba) were being caught and re-raised as ValidationError (400), which incorrectly represented issues like rate limits (429) or internal errors (500) as bad request parameters. This change: - Adds a handler to re-raise any REST framework APIException directly, preserving their status codes - Changes the generic exception handler to raise APIException (500) instead of ValidationError (400) - Adds tests to verify that REST framework exceptions are properly re-raised and generic exceptions return 500 This allows Seer to properly differentiate between client errors (4xx) and server errors (5xx) from downstream services. Co-authored-by: Andrew Liu --- src/sentry/seer/endpoints/seer_rpc.py | 4 +- tests/sentry/seer/endpoints/test_seer_rpc.py | 39 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index ec983725e07746..a95856b3a25488 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -269,13 +269,15 @@ def post(self, request: Request, method_name: str) -> Response: except SnubaRPCRateLimitExceeded as e: sentry_sdk.capture_exception() raise Throttled(detail="Rate limit exceeded") from e + except APIException: + raise except Exception as e: if in_test_environment(): raise if settings.DEBUG: raise Exception(f"Problem processing seer rpc endpoint {method_name}") from e sentry_sdk.capture_exception() - raise ValidationError from e + raise APIException from e return Response(data=result) diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index b7a52c6ac6f49c..2bdbcfb6626f85 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -84,6 +84,45 @@ def test_snuba_rate_limit_returns_429(self) -> None: assert response.status_code == 429 assert "Rate limit exceeded" in response.data["detail"] + def test_rest_framework_exceptions_are_reraised(self) -> None: + """Test that REST framework exceptions preserve their status codes.""" + from rest_framework.exceptions import APIException + + class CustomAPIException(APIException): + status_code = 503 + default_detail = "Service temporarily unavailable" + + path = self._get_path("get_organization_slug") + data: dict[str, Any] = {"args": {"org_id": 1}, "meta": {}} + + with patch( + "sentry.seer.endpoints.seer_rpc.SeerRpcServiceEndpoint._dispatch_to_local_method" + ) as mock_dispatch: + mock_dispatch.side_effect = CustomAPIException() + + response = self.client.post( + path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data) + ) + + assert response.status_code == 503 + assert "Service temporarily unavailable" in response.data["detail"] + + def test_generic_exceptions_return_500(self) -> None: + """Test that generic exceptions return 500 instead of 400.""" + path = self._get_path("get_organization_slug") + data: dict[str, Any] = {"args": {"org_id": 1}, "meta": {}} + + with patch( + "sentry.seer.endpoints.seer_rpc.SeerRpcServiceEndpoint._dispatch_to_local_method" + ) as mock_dispatch: + mock_dispatch.side_effect = RuntimeError("Unexpected internal error") + + response = self.client.post( + path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data) + ) + + assert response.status_code == 500 + class TestSeerRpcMethods(APITestCase): """Test individual RPC methods""" From 63b85e0772ade07b6dc827b20cd0b1f45c5e26bf Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:01:52 -0700 Subject: [PATCH 2/2] mock in_test_environ --- tests/sentry/seer/endpoints/test_seer_rpc.py | 25 ++++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index 2bdbcfb6626f85..cf8ce96d30ff84 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -112,16 +112,21 @@ def test_generic_exceptions_return_500(self) -> None: path = self._get_path("get_organization_slug") data: dict[str, Any] = {"args": {"org_id": 1}, "meta": {}} - with patch( - "sentry.seer.endpoints.seer_rpc.SeerRpcServiceEndpoint._dispatch_to_local_method" - ) as mock_dispatch: - mock_dispatch.side_effect = RuntimeError("Unexpected internal error") - - response = self.client.post( - path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data) - ) - - assert response.status_code == 500 + for is_test_environment in [True, False]: + with patch( + "sentry.seer.endpoints.seer_rpc.in_test_environment", + return_value=is_test_environment, + ): + with patch( + "sentry.seer.endpoints.seer_rpc.SeerRpcServiceEndpoint._dispatch_to_local_method" + ) as mock_dispatch: + mock_dispatch.side_effect = RuntimeError("Unexpected internal error") + + response = self.client.post( + path, data=data, HTTP_AUTHORIZATION=self.auth_header(path, data) + ) + + assert response.status_code == 500 class TestSeerRpcMethods(APITestCase):