Skip to content

Commit 2b7f7f1

Browse files
committed
ref(wsgi): Update _werkzeug vendor to newer version
Fixes GH-3516
1 parent 5a122b5 commit 2b7f7f1

File tree

2 files changed

+132
-34
lines changed

2 files changed

+132
-34
lines changed

sentry_sdk/_werkzeug.py

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,58 +41,89 @@
4141

4242

4343
#
44-
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
45-
# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361
44+
# `get_headers` comes from `werkzeug.datastructures.headers.__iter__`
45+
# https://github.com/pallets/werkzeug/blob/3.1.3/src/werkzeug/datastructures/headers.py#L644
4646
#
4747
# We need this function because Django does not give us a "pure" http header
4848
# dict. So we might as well use it for all WSGI integrations.
4949
#
5050
def _get_headers(environ):
5151
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
52-
"""
53-
Returns only proper HTTP headers.
54-
"""
5552
for key, value in environ.items():
56-
key = str(key)
57-
if key.startswith("HTTP_") and key not in (
53+
if key.startswith("HTTP_") and key not in {
5854
"HTTP_CONTENT_TYPE",
5955
"HTTP_CONTENT_LENGTH",
60-
):
56+
}:
6157
yield key[5:].replace("_", "-").title(), value
62-
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
58+
elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"} and value:
6359
yield key.replace("_", "-").title(), value
6460

6561

6662
#
6763
# `get_host` comes from `werkzeug.wsgi.get_host`
68-
# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145
64+
# https://github.com/pallets/werkzeug/blob/3.1.3/src/werkzeug/wsgi.py#L86
6965
#
7066
def get_host(environ, use_x_forwarded_for=False):
7167
# type: (Dict[str, str], bool) -> str
7268
"""
7369
Return the host for the given WSGI environment.
7470
"""
75-
if use_x_forwarded_for and "HTTP_X_FORWARDED_HOST" in environ:
76-
rv = environ["HTTP_X_FORWARDED_HOST"]
77-
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
78-
rv = rv[:-3]
79-
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
80-
rv = rv[:-4]
81-
elif environ.get("HTTP_HOST"):
82-
rv = environ["HTTP_HOST"]
83-
if environ["wsgi.url_scheme"] == "http" and rv.endswith(":80"):
84-
rv = rv[:-3]
85-
elif environ["wsgi.url_scheme"] == "https" and rv.endswith(":443"):
86-
rv = rv[:-4]
87-
elif environ.get("SERVER_NAME"):
88-
rv = environ["SERVER_NAME"]
89-
if (environ["wsgi.url_scheme"], environ["SERVER_PORT"]) not in (
90-
("https", "443"),
91-
("http", "80"),
92-
):
93-
rv += ":" + environ["SERVER_PORT"]
94-
else:
95-
# In spite of the WSGI spec, SERVER_NAME might not be present.
96-
rv = "unknown"
97-
98-
return rv
71+
return _get_host(
72+
environ["wsgi.url_scheme"],
73+
(
74+
environ["HTTP_X_FORWARDED_HOST"]
75+
if use_x_forwarded_for and environ.get("HTTP_X_FORWARDED_HOST")
76+
else environ.get("HTTP_HOST")
77+
),
78+
_get_server(environ),
79+
)
80+
81+
82+
# `_get_host` comes from `werkzeug.sansio.utils`
83+
# https://github.com/pallets/werkzeug/blob/3.1.3/src/werkzeug/sansio/utils.py#L49
84+
def _get_host(
85+
scheme,
86+
host_header,
87+
server=None,
88+
):
89+
# type: (str, str | None, Tuple[str, int | None] | None) -> str
90+
"""
91+
Return the host for the given parameters.
92+
"""
93+
host = ""
94+
95+
if host_header is not None:
96+
host = host_header
97+
elif server is not None:
98+
host = server[0]
99+
100+
# If SERVER_NAME is IPv6, wrap it in [] to match Host header.
101+
# Check for : because domain or IPv4 can't have that.
102+
if ":" in host and host[0] != "[":
103+
host = f"[{host}]"
104+
105+
if server[1] is not None:
106+
host = f"{host}:{server[1]}" # noqa: E231
107+
108+
if scheme in {"http", "ws"} and host.endswith(":80"):
109+
host = host[:-3]
110+
elif scheme in {"https", "wss"} and host.endswith(":443"):
111+
host = host[:-4]
112+
113+
return host
114+
115+
116+
def _get_server(environ):
117+
# type: (Dict[str, str]) -> Tuple[str, int | None] | None
118+
name = environ.get("SERVER_NAME")
119+
120+
if name is None:
121+
return None
122+
123+
try:
124+
port = int(environ.get("SERVER_PORT", None)) # type: ignore[arg-type]
125+
except (TypeError, ValueError):
126+
# unix socket
127+
port = None
128+
129+
return name, port

tests/integrations/wsgi/test_wsgi.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sentry_sdk
88
from sentry_sdk import capture_message
99
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
10+
from sentry_sdk._werkzeug import get_host
1011

1112

1213
@pytest.fixture
@@ -39,6 +40,50 @@ def next(self):
3940
return type(self).__next__(self)
4041

4142

43+
@pytest.mark.parametrize(
44+
("environ", "expect"),
45+
(
46+
pytest.param({"HTTP_HOST": "spam"}, "spam", id="host"),
47+
pytest.param({"HTTP_HOST": "spam:80"}, "spam", id="host, strip http port"),
48+
pytest.param(
49+
{"wsgi.url_scheme": "https", "HTTP_HOST": "spam:443"},
50+
"spam",
51+
id="host, strip https port",
52+
),
53+
pytest.param({"HTTP_HOST": "spam:8080"}, "spam:8080", id="host, custom port"),
54+
pytest.param(
55+
{"HTTP_HOST": "spam", "SERVER_NAME": "eggs", "SERVER_PORT": "80"},
56+
"spam",
57+
id="prefer host",
58+
),
59+
pytest.param(
60+
{"SERVER_NAME": "eggs", "SERVER_PORT": "80"},
61+
"eggs",
62+
id="name, ignore http port",
63+
),
64+
pytest.param(
65+
{"wsgi.url_scheme": "https", "SERVER_NAME": "eggs", "SERVER_PORT": "443"},
66+
"eggs",
67+
id="name, ignore https port",
68+
),
69+
pytest.param(
70+
{"SERVER_NAME": "eggs", "SERVER_PORT": "8080"},
71+
"eggs:8080",
72+
id="name, custom port",
73+
),
74+
pytest.param(
75+
{"HTTP_HOST": "ham", "HTTP_X_FORWARDED_HOST": "eggs"},
76+
"ham",
77+
id="ignore x-forwarded-host",
78+
),
79+
),
80+
)
81+
# https://github.com/pallets/werkzeug/blob/main/tests/test_wsgi.py#L60
82+
def test_get_host(environ, expect):
83+
environ.setdefault("wsgi.url_scheme", "http")
84+
assert get_host(environ) == expect
85+
86+
4287
def test_basic(sentry_init, crashing_app, capture_events):
4388
sentry_init(send_default_pii=True)
4489
app = SentryWsgiMiddleware(crashing_app)
@@ -61,6 +106,28 @@ def test_basic(sentry_init, crashing_app, capture_events):
61106
}
62107

63108

109+
def test_basic_forwarded_host(sentry_init, crashing_app, capture_events):
110+
sentry_init(send_default_pii=True)
111+
app = SentryWsgiMiddleware(crashing_app, use_x_forwarded_for=True)
112+
client = Client(app)
113+
events = capture_events()
114+
115+
with pytest.raises(ZeroDivisionError):
116+
client.get("/", environ_overrides={"HTTP_X_FORWARDED_HOST": "foobarbaz:80"})
117+
118+
(event,) = events
119+
120+
assert event["transaction"] == "generic WSGI request"
121+
122+
assert event["request"] == {
123+
"env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
124+
"headers": {"Host": "localhost", "X-Forwarded-Host": "foobarbaz:80"},
125+
"method": "GET",
126+
"query_string": "",
127+
"url": "http://foobarbaz/",
128+
}
129+
130+
64131
@pytest.mark.parametrize("path_info", ("bark/", "/bark/"))
65132
@pytest.mark.parametrize("script_name", ("woof/woof", "woof/woof/"))
66133
def test_script_name_is_respected(

0 commit comments

Comments
 (0)