Skip to content

Commit 05f9723

Browse files
stasspumer
authored andcommitted
fix: propagating exceptions from integration middleware
1 parent b4fc038 commit 05f9723

File tree

2 files changed

+123
-45
lines changed

2 files changed

+123
-45
lines changed

fastapi_jsonrpc/contrib/sentry/jrpc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ async def jrpc_transaction_middleware(ctx: JsonRpcContext):
7474
mechanism={"type": "asgi", "handled": False},
7575
)
7676
sentry_sdk.capture_event(event, hint=hint)
77+
# propagate error further. Possible duplicates would be suppressed by default `DedupeIntegration`
78+
raise exc from None
7779

7880

7981
class JrpcTransaction(Transaction):

tests/sentry/test_sentry_sdk_2x.py

Lines changed: 121 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import pytest
66
from logging import getLogger
77
from sentry_sdk.tracing import Transaction
8+
from fastapi_jsonrpc import BaseError
9+
810
from fastapi_jsonrpc.contrib.sentry.test_utils import (
911
get_transaction_trace_id,
1012
get_captured_transactions,
@@ -16,64 +18,123 @@
1618
pytest.skip(f"Testset is only for sentry_sdk 2.x, given {sentry_sdk_version=}", allow_module_level=True)
1719

1820

21+
class JrpcSampleError(BaseError):
22+
CODE = 5001
23+
MESSAGE = "Sample JRPC error"
24+
25+
1926
@pytest.fixture
2027
def failing_router(ep):
2128
sub_app = ep
2229
logger = getLogger("test-sentry")
2330

24-
@sub_app.method(name="first_failing_method")
25-
async def first_failing_method():
31+
@sub_app.method(name="first_logged_error_method")
32+
async def first_logged_error_method() -> dict:
2633
try:
2734
raise ValueError()
2835
except Exception:
29-
logger.exception("First route exc")
36+
logger.exception("First logged error method exc")
37+
38+
return {"handled": True}
3039

31-
@sub_app.method(name="second_failing_method")
32-
async def second_failing_method():
40+
@sub_app.method(name="second_logged_error_method")
41+
async def second_logged_error_method() -> dict:
3342
try:
3443
raise TypeError()
3544
except Exception:
36-
logger.exception("Second route exc")
45+
logger.exception("Second logged error method exc")
3746

38-
@sub_app.method(name="unhandled_error_method")
39-
async def unhandled_error_method():
40-
raise RuntimeError("Third route exc")
47+
return {"handled": True}
4148

42-
@sub_app.method(name="successful_method")
43-
async def successful_method():
44-
pass
49+
@sub_app.method(name="unhandled_error_method")
50+
async def unhandled_error_method() -> dict:
51+
raise RuntimeError("Unhandled method exc")
4552

53+
@sub_app.method(name="jrpc_error_method")
54+
async def jrpc_error_method() -> dict:
55+
raise JrpcSampleError()
4656

47-
def test_exception_logger_event_creation(
48-
json_request, capture_exceptions, capture_events, capture_envelopes, failing_router, sentry_with_integration,
49-
assert_log_errors,
57+
@sub_app.method(name="successful_method")
58+
async def successful_method() -> dict:
59+
return {"success": True}
60+
61+
62+
def test_logged_exceptions_event_creation(
63+
json_request,
64+
capture_exceptions,
65+
capture_events,
66+
capture_envelopes,
67+
failing_router,
68+
sentry_with_integration,
69+
assert_log_errors,
5070
):
5171
exceptions = capture_exceptions()
5272
envelops = capture_envelopes()
53-
json_request(
73+
response = json_request(
5474
[
55-
{"jsonrpc": "2.0", "method": "first_failing_method", "params": {}, "id": 1},
56-
{"jsonrpc": "2.0", "method": "second_failing_method", "params": {}, "id": 1},
75+
{
76+
"method": "first_logged_error_method",
77+
"params": {},
78+
"jsonrpc": "2.0",
79+
"id": 1,
80+
},
81+
{
82+
"method": "second_logged_error_method",
83+
"params": {},
84+
"jsonrpc": "2.0",
85+
"id": 1,
86+
},
5787
],
5888
)
89+
assert response == [
90+
{
91+
"result": {"handled": True},
92+
"jsonrpc": "2.0",
93+
"id": 1,
94+
},
95+
{
96+
"result": {"handled": True},
97+
"jsonrpc": "2.0",
98+
"id": 1,
99+
},
100+
]
59101
assert {type(e) for e in exceptions} == {ValueError, TypeError}
60102
assert_log_errors(
61-
'First route exc', pytest.raises(ValueError),
62-
'Second route exc', pytest.raises(TypeError),
103+
"First logged error method exc",
104+
pytest.raises(ValueError),
105+
"Second logged error method exc",
106+
pytest.raises(TypeError),
63107
)
64108
# 2 errors and 2 transactions
65109
assert_jrpc_batch_sentry_items(envelops, expected_items={"event": 2, "transaction": 2})
66110

67111

68-
def test_unhandled_exception_capturing(
69-
json_request, capture_exceptions, capture_events, capture_envelopes, failing_router, assert_log_errors,
70-
sentry_with_integration
112+
def test_unhandled_exception_event_creation(
113+
json_request,
114+
capture_exceptions,
115+
capture_events,
116+
capture_envelopes,
117+
failing_router,
118+
assert_log_errors,
119+
sentry_with_integration,
71120
):
72121
exceptions = capture_exceptions()
73122
envelops = capture_envelopes()
74-
json_request({"jsonrpc": "2.0", "method": "unhandled_error_method", "params": {}, "id": 1})
123+
response = json_request(
124+
{
125+
"method": "unhandled_error_method",
126+
"params": {},
127+
"jsonrpc": "2.0",
128+
"id": 1,
129+
}
130+
)
131+
assert response == {
132+
"error": {"code": -32603, "message": "Internal error"},
133+
"jsonrpc": "2.0",
134+
"id": 1,
135+
}
75136
assert_log_errors(
76-
"Third route exc",
137+
"Unhandled method exc",
77138
pytest.raises(RuntimeError),
78139
)
79140
assert {type(e) for e in exceptions} == {RuntimeError}
@@ -86,50 +147,65 @@ def test_unhandled_exception_capturing(
86147
[
87148
[
88149
{
89-
"jsonrpc": "2.0",
90-
"method": "first_failing_method",
150+
"method": "jrpc_error_method",
91151
"params": {},
152+
"jsonrpc": "2.0",
92153
"id": 1,
93154
},
94155
{
95-
"jsonrpc": "2.0",
96156
"method": "unhandled_error_method",
97157
"params": {},
158+
"jsonrpc": "2.0",
98159
"id": 1,
99160
},
100161
{
101-
"jsonrpc": "2.0",
102162
"method": "successful_method",
103163
"params": {},
164+
"jsonrpc": "2.0",
104165
"id": 1,
105166
},
106167
]
107168
],
108169
)
109170
def test_trace_id_propagation(
110-
request_payload, json_request, capture_exceptions, capture_events, capture_envelopes, failing_router,
111-
assert_log_errors, sentry_with_integration
171+
request_payload,
172+
json_request,
173+
capture_exceptions,
174+
capture_events,
175+
capture_envelopes,
176+
failing_router,
177+
assert_log_errors,
178+
sentry_with_integration,
112179
):
113180
envelops = capture_envelopes()
114181
expected_trace_id = uuid.uuid4().hex
115182
incoming_transaction = Transaction(trace_id=expected_trace_id)
116183
tracing_headers = list(incoming_transaction.iter_headers())
117-
json_request(
118-
request_payload,
119-
headers=tracing_headers,
120-
)
121-
assert_jrpc_batch_sentry_items(envelops, expected_items={"transaction": 3, "event": 2})
184+
185+
response = json_request(request_payload, headers=tracing_headers)
186+
187+
assert response == [
188+
{
189+
"error": {"code": 5001, "message": "Sample JRPC error"},
190+
"jsonrpc": "2.0",
191+
"id": 1,
192+
},
193+
{
194+
"error": {"code": -32603, "message": "Internal error"},
195+
"jsonrpc": "2.0",
196+
"id": 1,
197+
},
198+
{
199+
"result": {"success": True},
200+
"jsonrpc": "2.0",
201+
"id": 1,
202+
},
203+
]
204+
assert_jrpc_batch_sentry_items(envelops, expected_items={"transaction": 3, "event": 1})
122205
for transaction in get_captured_transactions(envelops):
123206
assert get_transaction_trace_id(transaction) == expected_trace_id
124207

125208
assert_log_errors(
126-
'First route exc', pytest.raises(ValueError),
127-
'Third route exc', pytest.raises(RuntimeError),
209+
"Unhandled method exc",
210+
pytest.raises(RuntimeError),
128211
)
129-
130-
131-
def test_no_integration_warning_message(json_request, sentry_no_integration):
132-
with pytest.warns(UserWarning, match="Implicit Sentry integration is deprecated"):
133-
json_request(
134-
{"jsonrpc": "2.0", "method": "successful_method", "params": {}, "id": 1}
135-
)

0 commit comments

Comments
 (0)