Skip to content

Commit 75b9810

Browse files
committed
ref(wsgi): Update _werkzeug vendor to newer version
Fixes GH-3516
1 parent dcefe38 commit 75b9810

File tree

2 files changed

+106
-32
lines changed

2 files changed

+106
-32
lines changed

sentry_sdk/_werkzeug.py

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -39,60 +39,112 @@
3939
from typing import Iterator
4040
from typing import Tuple
4141

42+
text_type = str
43+
iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs))
44+
4245

4346
#
44-
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders`
45-
# https://github.com/pallets/werkzeug/blob/0.14.1/werkzeug/datastructures.py#L1361
47+
# `get_headers` comes from `werkzeug.datastructures.EnvironHeaders.__iter__`
48+
# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/datastructures.py#L1470
4649
#
4750
# We need this function because Django does not give us a "pure" http header
4851
# dict. So we might as well use it for all WSGI integrations.
4952
#
5053
def _get_headers(environ):
5154
# type: (Dict[str, str]) -> Iterator[Tuple[str, str]]
52-
"""
53-
Returns only proper HTTP headers.
54-
"""
55-
for key, value in environ.items():
56-
key = str(key)
55+
for key, value in iteritems(environ):
5756
if key.startswith("HTTP_") and key not in (
5857
"HTTP_CONTENT_TYPE",
5958
"HTTP_CONTENT_LENGTH",
6059
):
61-
yield key[5:].replace("_", "-").title(), value
62-
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
63-
yield key.replace("_", "-").title(), value
60+
yield (
61+
key[5:].replace("_", "-").title(),
62+
_unicodify_header_value(value),
63+
)
64+
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH") and value:
65+
yield (key.replace("_", "-").title(), _unicodify_header_value(value))
66+
67+
68+
def _unicodify_header_value(value):
69+
# type: (str | bytes) -> str
70+
if isinstance(value, bytes):
71+
value = value.decode("latin-1")
72+
if not isinstance(value, text_type):
73+
value = text_type(value)
74+
return value
6475

6576

6677
#
6778
# `get_host` comes from `werkzeug.wsgi.get_host`
68-
# https://github.com/pallets/werkzeug/blob/1.0.1/src/werkzeug/wsgi.py#L145
79+
# https://github.com/pallets/werkzeug/blob/3.1.3/src/werkzeug/wsgi.py#L86
6980
#
7081
def get_host(environ, use_x_forwarded_for=False):
7182
# type: (Dict[str, str], bool) -> str
7283
"""
7384
Return the host for the given WSGI environment.
7485
"""
7586
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"]
87+
host = environ["HTTP_X_FORWARDED_HOST"]
88+
if environ.get("wsgi.url_scheme") == "http" and host.endswith(":80"):
89+
host = host[:-3]
90+
elif environ.get("wsgi.url_scheme") == "https" and host.endswith(":443"):
91+
host = host[:-4]
9492
else:
95-
# In spite of the WSGI spec, SERVER_NAME might not be present.
96-
rv = "unknown"
93+
host = _get_host(
94+
environ["wsgi.url_scheme"],
95+
environ.get("HTTP_HOST"),
96+
_get_server(environ),
97+
)
98+
99+
return host
100+
101+
102+
# `_get_host` comes from `werkzeug.sansio.utils`
103+
# https://github.com/pallets/werkzeug/blob/3.1.3/src/werkzeug/sansio/utils.py#L49
104+
def _get_host(
105+
scheme: str,
106+
host_header: str | None,
107+
server: tuple[str, int | None] | None = None,
108+
) -> str:
109+
"""
110+
Return the host for the given parameters.
111+
"""
112+
113+
host = ""
114+
115+
if host_header is not None:
116+
host = host_header
117+
elif server is not None:
118+
host = server[0]
119+
120+
# If SERVER_NAME is IPv6, wrap it in [] to match Host header.
121+
# Check for : because domain or IPv4 can't have that.
122+
if ":" in host and host[0] != "[":
123+
host = f"[{host}]"
124+
125+
if server[1] is not None:
126+
host = f"{host}:{server[1]}" # noqa: E231
127+
128+
if scheme in {"http", "ws"} and host.endswith(":80"):
129+
host = host[:-3]
130+
elif scheme in {"https", "wss"} and host.endswith(":443"):
131+
host = host[:-4]
132+
133+
return host
134+
135+
136+
def _get_server(
137+
environ, # type: Dict[str, str]
138+
) -> tuple[str, int | None] | None:
139+
name = environ.get("SERVER_NAME")
140+
141+
if name is None:
142+
return None
143+
144+
try:
145+
port: int | None = int(environ.get("SERVER_PORT", None))
146+
except (TypeError, ValueError):
147+
# unix socket
148+
port = None
97149

98-
return rv
150+
return name, port

tests/integrations/wsgi/test_wsgi.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,28 @@ def test_basic(sentry_init, crashing_app, capture_events):
6161
}
6262

6363

64+
def test_basic_django(sentry_init, crashing_app, capture_events):
65+
sentry_init(send_default_pii=True)
66+
app = SentryWsgiMiddleware(crashing_app, use_x_forwarded_for=True)
67+
client = Client(app)
68+
events = capture_events()
69+
70+
with pytest.raises(ZeroDivisionError):
71+
client.get("/", environ_overrides={"HTTP_X_FORWARDED_HOST": "localhost:80"})
72+
73+
(event,) = events
74+
75+
assert event["transaction"] == "generic WSGI request"
76+
77+
assert event["request"] == {
78+
"env": {"SERVER_NAME": "localhost", "SERVER_PORT": "80"},
79+
"headers": {"Host": "localhost", "X-Forwarded-Host": "localhost:80"},
80+
"method": "GET",
81+
"query_string": "",
82+
"url": "http://localhost/",
83+
}
84+
85+
6486
@pytest.mark.parametrize("path_info", ("bark/", "/bark/"))
6587
@pytest.mark.parametrize("script_name", ("woof/woof", "woof/woof/"))
6688
def test_script_name_is_respected(

0 commit comments

Comments
 (0)