diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 1576e21a17..ea7ebbea4d 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -86,7 +86,7 @@ def __init__( 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) @@ -135,6 +135,29 @@ def __call__( 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). + # + # Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects, + # so we want to make sure we don't interfere with those either. + # + # If all conditions are met, we return the original response object directly, + # allowing uWSGI to handle it as intended. + if ( + environ.get("wsgi.file_wrapper") + and getattr(response, "fileno", None) is not None + ): + return response + return _ScopedResponse(scope, response) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index a741d1c57b..1878be4866 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -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 @@ -500,3 +500,50 @@ def dogpark(environ, start_response): (event,) = events assert event["contexts"]["trace"]["origin"] == "auto.dogpark.deluxe" + + +@pytest.mark.parametrize( + "has_file_wrapper, has_fileno, expect_wrapped", + [ + (True, True, False), # both conditions met → unwrapped + (False, True, True), # no file_wrapper → wrapped + (True, False, True), # no fileno → wrapped + (False, False, True), # neither condition → wrapped + ], +) +def test_file_response_wrapping( + sentry_init, 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) + + 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