Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/sentry/hybridcloud/apigateway/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from django.http.response import HttpResponseBase
from requests import Response as ExternalResponse
from requests import request as external_request
from requests.exceptions import Timeout
from requests.exceptions import ConnectionError, Timeout

from sentry import options
from sentry.api.exceptions import RequestTimeout
Expand Down Expand Up @@ -193,18 +193,22 @@ def proxy_cell_request(request: HttpRequest, cell: Cell, url_name: str) -> HttpR
)
except Timeout:
metrics.incr("apigateway.proxy.request_timeout", tags=metric_tags)
try:
if circuit_breaker is not None:
circuit_breaker.record_error()
except Exception:
logger.exception("Failed to record circuitbreaker failure")
if circuit_breaker is not None:
circuit_breaker.record_error()
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.

Removed try/except allows record_error to suppress original exceptions

Medium Severity

The try/except around circuit_breaker.record_error() was removed based on the assumption that record_error() handles its own exceptions internally. However, record_error() calls self.limiter.use_quotas() and self.limiter.check_within_quotas() (via _should_trip) — both unprotected Redis operations that can throw. If Redis is flaky, an exception from record_error() in the Timeout handler prevents RequestTimeout() from being raised, and in the ConnectionError handler it replaces the original ConnectionError with an unrelated Redis exception.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The record_error() method traps errors.


# remote silo timeout. Use DRF timeout instead
raise RequestTimeout()
except ConnectionError:
metrics.incr("apigateway.proxy.connection_error", tags=metric_tags)
if circuit_breaker is not None:
circuit_breaker.record_error()

raise

if resp.status_code >= 500 and circuit_breaker is not None:
if resp.status_code >= 502:
metrics.incr("apigateway.proxy.request_failed", tags=metric_tags)
circuit_breaker.record_error()
if circuit_breaker is not None:
circuit_breaker.record_error()

new_headers = clean_outbound_headers(resp.headers)
resp.headers.clear()
Expand Down
57 changes: 54 additions & 3 deletions tests/sentry/hybridcloud/apigateway/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import HttpResponse
from django.test.client import RequestFactory
from requests.exceptions import Timeout
from requests.exceptions import ConnectionError, Timeout

from sentry.api.exceptions import RequestTimeout
from sentry.hybridcloud.apigateway.proxy import proxy_request
Expand Down Expand Up @@ -352,7 +352,58 @@ def test_timeout_records_error(self) -> None:

@responses.activate
@override_options(CB_ENABLED)
def test_5xx_response_records_error(self) -> None:
def test_connection_error_records_error(self) -> None:
responses.add(
responses.GET,
f"{self.CELL.address}/connect-error",
body=ConnectionError(),
)
with patch("sentry.hybridcloud.apigateway.proxy.CircuitBreaker") as mock_breaker_class:
mock_breaker = self._make_breaker_mock(allow_request=True)
mock_breaker_class.return_value = mock_breaker
request = RequestFactory().get("http://sentry.io/connect-error")
with pytest.raises(ConnectionError):
proxy_request(request, self.organization.slug, url_name)
mock_breaker.record_error.assert_called_once()

@responses.activate
@override_options(CB_ENABLED)
def test_connection_error_records_metric(self) -> None:
responses.add(
responses.GET,
f"{self.CELL.address}/connect-error",
body=ConnectionError(),
)
with patch("sentry.hybridcloud.apigateway.proxy.metrics") as mock_metrics:
request = RequestFactory().get("http://sentry.io/connect-error")
with pytest.raises(ConnectionError):
proxy_request(request, self.organization.slug, url_name)
mock_metrics.incr.assert_any_call(
"apigateway.proxy.connection_error",
tags={"region": self.CELL.name, "url_name": url_name},
)

@responses.activate
@override_options(CB_ENABLED)
def test_504_response_does_record_error(self) -> None:
responses.add(
responses.GET,
f"{self.CELL.address}/server-error",
status=504,
body=json.dumps({"detail": "gateway timeout"}),
content_type="application/json",
)
with patch("sentry.hybridcloud.apigateway.proxy.CircuitBreaker") as mock_breaker_class:
mock_breaker = self._make_breaker_mock(allow_request=True)
mock_breaker_class.return_value = mock_breaker
request = RequestFactory().get("http://sentry.io/server-error")
resp = proxy_request(request, self.organization.slug, url_name)
assert resp.status_code == 504
mock_breaker.record_error.assert_called_once()
Comment thread
cursor[bot] marked this conversation as resolved.

@responses.activate
@override_options(CB_ENABLED)
def test_500_response_does_not_record_error(self) -> None:
responses.add(
responses.GET,
f"{self.CELL.address}/server-error",
Expand All @@ -366,7 +417,7 @@ def test_5xx_response_records_error(self) -> None:
request = RequestFactory().get("http://sentry.io/server-error")
resp = proxy_request(request, self.organization.slug, url_name)
assert resp.status_code == 500
mock_breaker.record_error.assert_called_once()
mock_breaker.record_error.assert_not_called()

@responses.activate
@override_options(CB_ENABLED)
Expand Down
Loading