Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 43 additions & 1 deletion sentry_sdk/integrations/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

def __call__(
self, environ: "Dict[str, str]", start_response: "Callable[..., Any]"
) -> "_ScopedResponse":
) -> "Any":
if _wsgi_middleware_applied.get(False):
return self.app(environ, start_response)

Expand Down Expand Up @@ -135,9 +135,51 @@
finally:
_wsgi_middleware_applied.set(False)

# Within the uWSGI subhandler, the use of the "offload" mechanism for file responses
# is determined by a pointer equality check on the response object
# (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278).
#
# If we were to return a _ScopedResponse, this would cause the check to always fail
# since it's checking the files are exactly the same.
#
# To avoid this and ensure that the offloading mechanism works as expected when it's
# enabled, we check if the response is a file-like object (determined by the presence
# of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so,
# it would've been used in handling the file in the response), and if uWSGI's
# offload-threads option is configured (since offloading only occurs when offload
# threads are enabled).
#
# If all conditions are met, we return the original response object directly,
# allowing uWSGI to handle it as intended.
if (
_is_uwsgi_offload_threads_enabled()
and environ.get("wsgi.file_wrapper")
and getattr(response, "fileno", None) is not None
):
Comment thread
ericapisani marked this conversation as resolved.
return response

Check warning on line 159 in sentry_sdk/integrations/wsgi.py

View workflow job for this annotation

GitHub Actions / warden: code-review

Skipping _ScopedResponse loses exception capture for file response iteration

When the uWSGI offload optimization is detected and the raw response is returned directly (line 159), exceptions that occur during iteration of the file response will not be captured by Sentry. The `_ScopedResponse` wrapper normally catches exceptions in `__iter__` and `close` methods via `_capture_exception()`. This is an intentional trade-off documented in the comments, but represents a behavioral change where errors in file streaming won't be reported to Sentry when uWSGI offload is enabled.
Comment thread
github-actions[bot] marked this conversation as resolved.

return _ScopedResponse(scope, response)


def _is_uwsgi_offload_threads_enabled() -> bool:
try:
from uwsgi import opt
except ImportError:
return False

value = opt.get("offload-threads") or opt.get(b"offload-threads")
if not value:
return False
if isinstance(value, bytes):
try:
return int(value.decode()) > 0
except (ValueError, UnicodeDecodeError):
return False
if isinstance(value, int):
return value > 0
return False


def _sentry_start_response(
old_start_response: "StartResponse",
transaction: "Optional[Transaction]",
Expand Down
65 changes: 64 additions & 1 deletion tests/integrations/wsgi/test_wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware, _ScopedResponse


@pytest.fixture
Expand Down Expand Up @@ -500,3 +500,66 @@ def dogpark(environ, start_response):
(event,) = events

assert event["contexts"]["trace"]["origin"] == "auto.dogpark.deluxe"


@pytest.mark.parametrize(
"uwsgi_opt, has_file_wrapper, has_fileno, expect_wrapped",
[
({"offload-threads": 1}, True, True, False), # all conditions met → unwrapped
({"offload-threads": 0}, True, True, True), # offload disabled → wrapped
({"offload-threads": 1}, False, True, True), # no file_wrapper → wrapped
({"offload-threads": 1}, True, False, True), # no fileno → wrapped
(None, True, True, True), # uwsgi not installed → wrapped
({"offload-threads": b"1"}, True, True, False), # bytes value → unwrapped
(
{b"offload-threads": b"1"},
True,
True,
False,
), # bytes key + bytes value → unwrapped
],
)
def test_uwsgi_offload_threads_response_wrapping(
sentry_init, uwsgi_opt, has_file_wrapper, has_fileno, expect_wrapped
):
sentry_init()

response_mock = mock.MagicMock()
if not has_fileno:
del response_mock.fileno

def app(environ, start_response):
start_response("200 OK", [])
return response_mock

environ_extra = {}
if has_file_wrapper:
environ_extra["wsgi.file_wrapper"] = mock.MagicMock()

middleware = SentryWsgiMiddleware(app)

if uwsgi_opt is not None:
uwsgi_mock = mock.MagicMock()
uwsgi_mock.opt = uwsgi_opt
patch_ctx = mock.patch.dict("sys.modules", uwsgi=uwsgi_mock)
else:
patch_ctx = mock.patch.dict("sys.modules", {"uwsgi": None})

with patch_ctx:
result = middleware(
{
"REQUEST_METHOD": "GET",
"PATH_INFO": "/",
"SERVER_NAME": "localhost",
"SERVER_PORT": "80",
"wsgi.url_scheme": "http",
"wsgi.input": mock.MagicMock(),
**environ_extra,
},
lambda status, headers: None,
)

if expect_wrapped:
assert isinstance(result, _ScopedResponse)
else:
assert result is response_mock
Loading