Skip to content

fix(rpc): fix seer RPC exception handling to default to 500 and preserve rest APIException#112435

Merged
aliu39 merged 2 commits intomasterfrom
cursor/seer-rpc-rest-framework-exceptions-b045
Apr 11, 2026
Merged

fix(rpc): fix seer RPC exception handling to default to 500 and preserve rest APIException#112435
aliu39 merged 2 commits intomasterfrom
cursor/seer-rpc-rest-framework-exceptions-b045

Conversation

@aliu39
Copy link
Copy Markdown
Member

@aliu39 aliu39 commented Apr 7, 2026

Problem

When Seer makes RPC calls to Sentry and encounters downstream errors (like 429 rate limits or 500 errors from Snuba), the current code catches all exceptions and re-raises them as ValidationError (400). This misrepresents the actual issue, making it appear as if the client sent bad request parameters when the real cause is a downstream service error.

Example issue: https://sentry.dev.getsentry.net:7999/issues/7380803020/events/d3e6e1d5ddf44d7b94c86e6ce2435ea6/

Solution

This PR fixes the exception handling in the seer RPC endpoint to raise a generic 500 APIException by default, implying an error happened on sentry server. Also re-raises APIException's (base class for rest framework excs) so developers can bubble their own status codes up to response for Seer.

  1. Added a handler to re-raise REST framework exceptions directly - Any APIException (which includes Throttled, custom API exceptions, etc.) is now re-raised as-is, preserving its status code
  2. Changed generic exception handler to raise APIException (500) instead of ValidationError (400) - Unknown errors now correctly return 500 Internal Server Error instead of 400 Bad Request

Changes

  • Modified SeerRpcServiceEndpoint.post() in src/sentry/seer/endpoints/seer_rpc.py:
    • Added except APIException: raise handler before the generic exception handler
    • Changed raise ValidationError from e to raise APIException from e for generic exceptions
  • Added test cases in tests/sentry/seer/endpoints/test_seer_rpc.py:
    • test_rest_framework_exceptions_are_reraised - verifies custom API exceptions preserve their status codes
    • test_generic_exceptions_return_500 - verifies generic exceptions return 500 instead of 400

Testing

Pre-commit checks pass ✅

Slack Thread

Open in Web Open in Cursor 

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 <aliu39@users.noreply.github.com>
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 7, 2026
@aliu39 aliu39 marked this pull request as ready for review April 8, 2026 00:26
@aliu39 aliu39 requested a review from a team as a code owner April 8, 2026 00:26
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Test doesn't exercise the new APIException code path
    • Mocked in_test_environment() to return False so the test properly exercises the APIException conversion code path instead of re-raising the original exception.

Create PR

Or push these changes by commenting:

@cursor push bbfaa3b8be
Preview (bbfaa3b8be)
diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py
--- a/tests/sentry/seer/endpoints/test_seer_rpc.py
+++ b/tests/sentry/seer/endpoints/test_seer_rpc.py
@@ -112,9 +112,12 @@
         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:
+        with (
+            patch(
+                "sentry.seer.endpoints.seer_rpc.SeerRpcServiceEndpoint._dispatch_to_local_method"
+            ) as mock_dispatch,
+            patch("sentry.seer.endpoints.seer_rpc.in_test_environment", return_value=False),
+        ):
             mock_dispatch.side_effect = RuntimeError("Unexpected internal error")
 
             response = self.client.post(

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 388d7ab. Configure here.

Comment thread tests/sentry/seer/endpoints/test_seer_rpc.py Outdated
@aliu39 aliu39 changed the title Fix seer RPC exception handling to preserve HTTP status codes fix(rpc): fix seer RPC exception handling to default to 500 and preserve rest APIException Apr 8, 2026
@aliu39
Copy link
Copy Markdown
Member Author

aliu39 commented Apr 8, 2026

Re cursor: not sure how to simulate non-test environment from a test.. code seems straightforward enough to not cover

Copy link
Copy Markdown
Member

@trevor-e trevor-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: testing, I think you could patch in_test_environment() to return false?

Comment on lines +119 to +129
):
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The test test_generic_exceptions_return_500 crashes when in_test_environment() is true because a raw RuntimeError is re-raised, preventing the response assertion from being executed.
Severity: LOW

Suggested Fix

The test should be modified to handle the expected RuntimeError when in_test_environment() returns True. This can be done by wrapping the self.client.post(...) call in a pytest.raises(RuntimeError) context manager for the test environment case. This will correctly assert that the exception is raised as intended, allowing the test to pass and properly validate the code path.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: tests/sentry/seer/endpoints/test_seer_rpc.py#L115-L129

Potential issue: The test `test_generic_exceptions_return_500` is designed to check for
a 500 error response. However, it loops through `is_test_environment` being `True` and
`False`. When `is_test_environment` is mocked to `True`, the code under test re-raises a
raw `RuntimeError`. This exception is not handled by the DRF exception handler.
Consequently, the Django test client re-raises the exception, causing the test to crash
at the `self.client.post(...)` call. The assertion `assert response.status_code == 500`
is never reached because the `response` variable is never assigned. This means the test
fails to validate the intended behavior for the test environment scenario.

Did we get this right? 👍 / 👎 to inform future reviews.

@aliu39 aliu39 merged commit 32333c6 into master Apr 11, 2026
56 checks passed
@aliu39 aliu39 deleted the cursor/seer-rpc-rest-framework-exceptions-b045 branch April 11, 2026 05:20
wedamija pushed a commit that referenced this pull request Apr 13, 2026
…rve rest APIException (#112435)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Problem

When Seer makes RPC calls to Sentry and encounters downstream errors
(like 429 rate limits or 500 errors from Snuba), the current code
catches all exceptions and re-raises them as `ValidationError` (400).
This misrepresents the actual issue, making it appear as if the client
sent bad request parameters when the real cause is a downstream service
error.

Example issue:
https://sentry.dev.getsentry.net:7999/issues/7380803020/events/d3e6e1d5ddf44d7b94c86e6ce2435ea6/

## Solution

This PR fixes the exception handling in the seer RPC endpoint to raise a
generic 500 APIException by default, implying an error happened on
sentry server. Also re-raises APIException's (base class for rest
framework excs) so developers can bubble their own status codes up to
response for Seer.

1. **Added a handler to re-raise REST framework exceptions directly** -
Any `APIException` (which includes `Throttled`, custom API exceptions,
etc.) is now re-raised as-is, preserving its status code
2. **Changed generic exception handler to raise `APIException` (500)
instead of `ValidationError` (400)** - Unknown errors now correctly
return 500 Internal Server Error instead of 400 Bad Request

### Changes

- Modified `SeerRpcServiceEndpoint.post()` in
`src/sentry/seer/endpoints/seer_rpc.py`:
- Added `except APIException: raise` handler before the generic
exception handler
- Changed `raise ValidationError from e` to `raise APIException from e`
for generic exceptions
- Added test cases in `tests/sentry/seer/endpoints/test_seer_rpc.py`:
- `test_rest_framework_exceptions_are_reraised` - verifies custom API
exceptions preserve their status codes
- `test_generic_exceptions_return_500` - verifies generic exceptions
return 500 instead of 400

## Testing

Pre-commit checks pass ✅

<!-- CURSOR_AGENT_PR_BODY_END -->

[Slack
Thread](https://sentry.slack.com/archives/C0AAF0JD0GK/p1775600719171269?thread_ts=1775600719.171269&cid=C0AAF0JD0GK)

<div><a
href="https://cursor.com/agents/bc-8bf9c0b1-d369-5efa-81a7-a650bb7e83cc"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-8bf9c0b1-d369-5efa-81a7-a650bb7e83cc"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Andrew Liu <aliu39@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants