Skip to content

Commit c56f1f7

Browse files
ericapisaniclaude
andauthored
fix(wsgi): Do not wrap file responses when uWSGI offload-threads is enabled (#5556)
uWSGI uses pointer equality to detect file-like response objects eligible for its offload-threads optimization. When Sentry's WSGI middleware wraps the response in `_ScopedResponse`, the pointer changes and uWSGI's check always fails, silently disabling the offloading. This fix skips the `_ScopedResponse` wrapping when all three conditions are true: - uWSGI's `offload-threads` option is configured and > 0 - `wsgi.file_wrapper` is present in the WSGI environ (indicating the server would use it) - The response object has a `fileno` attribute (indicating it is a file-like object) When these conditions are met, the original response is returned directly, allowing uWSGI to handle offloading as intended. Fixes PY-1977 and #5107 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8628732 commit c56f1f7

File tree

2 files changed

+72
-2
lines changed

2 files changed

+72
-2
lines changed

sentry_sdk/integrations/wsgi.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def __init__(
8686

8787
def __call__(
8888
self, environ: "Dict[str, str]", start_response: "Callable[..., Any]"
89-
) -> "_ScopedResponse":
89+
) -> "Any":
9090
if _wsgi_middleware_applied.get(False):
9191
return self.app(environ, start_response)
9292

@@ -135,6 +135,29 @@ def __call__(
135135
finally:
136136
_wsgi_middleware_applied.set(False)
137137

138+
# Within the uWSGI subhandler, the use of the "offload" mechanism for file responses
139+
# is determined by a pointer equality check on the response object
140+
# (see https://github.com/unbit/uwsgi/blob/8d116f7ea2b098c11ce54d0b3a561c54dcd11929/plugins/python/wsgi_subhandler.c#L278).
141+
#
142+
# If we were to return a _ScopedResponse, this would cause the check to always fail
143+
# since it's checking the files are exactly the same.
144+
#
145+
# To avoid this and ensure that the offloading mechanism works as expected when it's
146+
# enabled, we check if the response is a file-like object (determined by the presence
147+
# of `fileno`), if the wsgi.file_wrapper is available in the environment (as if so,
148+
# it would've been used in handling the file in the response).
149+
#
150+
# Even if the offload mechanism is not enabled, there are optimizations that uWSGI does for file-like objects,
151+
# so we want to make sure we don't interfere with those either.
152+
#
153+
# If all conditions are met, we return the original response object directly,
154+
# allowing uWSGI to handle it as intended.
155+
if (
156+
environ.get("wsgi.file_wrapper")
157+
and getattr(response, "fileno", None) is not None
158+
):
159+
return response
160+
138161
return _ScopedResponse(scope, response)
139162

140163

tests/integrations/wsgi/test_wsgi.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import sentry_sdk
88
from sentry_sdk import capture_message
9-
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
9+
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware, _ScopedResponse
1010

1111

1212
@pytest.fixture
@@ -500,3 +500,50 @@ def dogpark(environ, start_response):
500500
(event,) = events
501501

502502
assert event["contexts"]["trace"]["origin"] == "auto.dogpark.deluxe"
503+
504+
505+
@pytest.mark.parametrize(
506+
"has_file_wrapper, has_fileno, expect_wrapped",
507+
[
508+
(True, True, False), # both conditions met → unwrapped
509+
(False, True, True), # no file_wrapper → wrapped
510+
(True, False, True), # no fileno → wrapped
511+
(False, False, True), # neither condition → wrapped
512+
],
513+
)
514+
def test_file_response_wrapping(
515+
sentry_init, has_file_wrapper, has_fileno, expect_wrapped
516+
):
517+
sentry_init()
518+
519+
response_mock = mock.MagicMock()
520+
if not has_fileno:
521+
del response_mock.fileno
522+
523+
def app(environ, start_response):
524+
start_response("200 OK", [])
525+
return response_mock
526+
527+
environ_extra = {}
528+
if has_file_wrapper:
529+
environ_extra["wsgi.file_wrapper"] = mock.MagicMock()
530+
531+
middleware = SentryWsgiMiddleware(app)
532+
533+
result = middleware(
534+
{
535+
"REQUEST_METHOD": "GET",
536+
"PATH_INFO": "/",
537+
"SERVER_NAME": "localhost",
538+
"SERVER_PORT": "80",
539+
"wsgi.url_scheme": "http",
540+
"wsgi.input": mock.MagicMock(),
541+
**environ_extra,
542+
},
543+
lambda status, headers: None,
544+
)
545+
546+
if expect_wrapped:
547+
assert isinstance(result, _ScopedResponse)
548+
else:
549+
assert result is response_mock

0 commit comments

Comments
 (0)