From 47d563cdb05d49ea4ef98a8d1fc21080cad05d8d Mon Sep 17 00:00:00 2001 From: Marcelo Galigniana Date: Mon, 18 Aug 2025 00:33:05 -0300 Subject: [PATCH] ref(wsgi): Delete custom `_werkzeug` and use new Werkzeug version Fixes: GH-3516 --- sentry_sdk/_werkzeug.py | 98 ---------------------------- sentry_sdk/integrations/wsgi.py | 16 ++++- tests/integrations/wsgi/test_wsgi.py | 22 +++++++ 3 files changed, 35 insertions(+), 101 deletions(-) delete mode 100644 sentry_sdk/_werkzeug.py diff --git a/sentry_sdk/_werkzeug.py b/sentry_sdk/_werkzeug.py deleted file mode 100644 index 0fa3d611f1..0000000000 --- a/sentry_sdk/_werkzeug.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Copyright (c) 2007 by the Pallets team. - -Some rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - -* Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND -CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, -BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF -USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. -""" - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict - from typing import Iterator - from typing import Tuple - - -# -# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders` -# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361 -# -# We need this function because Django does not give us a "pure" http header -# dict. So we might as well use it for all WSGI integrations. -# -def _get_headers(environ): - # type: (Dict[str, str]) -> Iterator[Tuple[str, str]] - """ - Returns only proper HTTP headers. - """ - for key, value in environ.items(): - key = str(key) - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield key[5:].replace("_", "-").title(), value - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - yield key.replace("_", "-").title(), value - - -# -# `get_host` comes from `werkzeug.wsgi.get_host` -# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145 -# -def get_host(environ, use_x_forwarded_for=False): - # type: (Dict[str, str], bool) -> str - """ - Return the host for the given WSGI environment. - """ - if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: - rv = environ["HTTP_X_FORWARDED_HOST"] - if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): - rv = rv[:-3] - elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): - rv = rv[:-4] - elif environ.get("HTTP_HOST"): - rv = environ["HTTP_HOST"] - if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"): - rv = rv[:-3] - elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"): - rv = rv[:-4] - elif environ.get("SERVER_NAME"): - rv = environ["SERVER_NAME"] - if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in ( - ("https", "443"), - ("http", "80"), - ): - rv += ":" + environ["SERVER_PORT"] - else: - # In spite of the WSGI spec, SERVER_NAME might not be present. - rv = "unknown" - - return rv diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index e628e50e69..bf08cbf349 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -2,7 +2,8 @@ from functools import partial import sentry_sdk -from sentry_sdk._werkzeug import get_host, _get_headers +from werkzeug.datastructures import EnvironHeaders +from werkzeug.wsgi import get_host from sentry_sdk.api import continue_trace from sentry_sdk.consts import OP from sentry_sdk.scope import should_send_default_pii @@ -62,9 +63,18 @@ def get_request_url(environ, use_x_forwarded_for=False): path_info = environ.get("PATH_INFO", "").lstrip("/") path = f"{script_name}/{path_info}" + if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ: + host = environ["HTTP_X_FORWARDED_HOST"] + if environ.get("wsgi.url_scheme") == "http" and host.endswith(":80"): + host = host[:-3] + elif environ.get("wsgi.url_scheme") == "https" and host.endswith(":443"): + host = host[:-4] + else: + host = get_host(environ) + return "%s://%s/%s" % ( environ.get("wsgi.url_scheme"), - get_host(environ, use_x_forwarded_for), + host, wsgi_decoding_dance(path).lstrip("/"), ) @@ -286,7 +296,7 @@ def _make_wsgi_event_processor(environ, use_x_forwarded_for): query_string = environ.get("QUERY_STRING") method = environ.get("REQUEST_METHOD") env = dict(_get_environ(environ)) - headers = _filter_headers(dict(_get_headers(environ))) + headers = _filter_headers(dict(EnvironHeaders(environ))) def event_processor(event, hint): # type: (Event, Dict[str, Any]) -> Event diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 656fc1757f..b0158529e0 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -61,6 +61,28 @@ def test_basic(sentry_init, crashing_app, capture_events): } +def test_basic_django(sentry_init, crashing_app, capture_events): + sentry_init(send_default_pii=True) + app = SentryWsgiMiddleware(crashing_app, use_x_forwarded_for=True) + client = Client(app) + events = capture_events() + + with pytest.raises(ZeroDivisionError): + client.get("/", environ_overrides={"HTTP_X_FORWARDED_HOST": "localhost:80"}) + + (event,) = events + + assert event["transaction"] == "generic WSGI request" + + assert event["request"] == { + "env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"}, + "headers": {"Host": "localhost", "X-Forwarded-Host": "localhost:80"}, + "method": "GET", + "query_string": "", + "url": "http://localhost/", + } + + @pytest.mark.parametrize("path_info", ("bark/", "/bark/")) @pytest.mark.parametrize("script_name", ("woof/woof", "woof/woof/")) def test_script_name_is_respected(