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
25 changes: 24 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 __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)

Expand Down Expand Up @@ -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)


Expand Down
49 changes: 48 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,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
Loading