Skip to content
Draft
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
293 changes: 128 additions & 165 deletions tests/integrations/starlette/test_starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.integrations.starlette import (
StarletteIntegration,
StarletteRequestExtractor,
)
from sentry_sdk.utils import parse_version
from tests.integrations.conftest import parametrize_test_configurable_status_codes
Expand Down Expand Up @@ -150,6 +149,21 @@ async def _render_template(request):
else:
return templates.TemplateResponse("trace_meta.html", template_context)

async def _body_json(request):
await request.json()
capture_message("hi")
return starlette.responses.JSONResponse({"status": "ok"})

async def _body_form(request):
await request.form()
capture_message("hi")
return starlette.responses.JSONResponse({"status": "ok"})

async def _body_raw(request):
await request.body()
capture_message("hi")
return starlette.responses.JSONResponse({"status": "ok"})

all_methods = [
"CONNECT",
"DELETE",
Expand All @@ -173,6 +187,9 @@ async def _render_template(request):
starlette.routing.Route("/sync/thread_ids", _thread_ids_sync),
starlette.routing.Route("/async/thread_ids", _thread_ids_async),
starlette.routing.Route("/render_template", _render_template),
starlette.routing.Route("/body/json", _body_json, methods=["POST"]),
starlette.routing.Route("/body/form", _body_form, methods=["POST"]),
starlette.routing.Route("/body/raw", _body_raw, methods=["POST"]),
],
middleware=middleware,
)
Expand Down Expand Up @@ -287,196 +304,144 @@ async def my_send(*args, **kwargs):


@pytest.mark.asyncio
async def test_starletterequestextractor_content_length(sentry_init):
scope = SCOPE.copy()
scope["headers"] = [
[b"content-length", str(len(json.dumps(BODY_JSON))).encode()],
]
starlette_request = starlette.requests.Request(scope)
extractor = StarletteRequestExtractor(starlette_request)
async def test_request_info_json_body(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[StarletteIntegration()],
)

assert await extractor.content_length() == len(json.dumps(BODY_JSON))
starlette_app = starlette_app_factory()
events = capture_events()

client = TestClient(starlette_app)
client.post(
"/body/json",
json=BODY_JSON,
headers={
"cookie": "yummy_cookie=choco; tasty_cookie=strawberry",
},
)

@pytest.mark.asyncio
async def test_starletterequestextractor_cookies(sentry_init):
starlette_request = starlette.requests.Request(SCOPE)
extractor = StarletteRequestExtractor(starlette_request)
(event, transaction_event) = events

assert extractor.cookies() == {
assert event["request"]["cookies"] == {
"tasty_cookie": "strawberry",
"yummy_cookie": "choco",
}
assert event["request"]["data"] == BODY_JSON


@pytest.mark.asyncio
async def test_starletterequestextractor_json(sentry_init):
starlette_request = starlette.requests.Request(SCOPE)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)

extractor = StarletteRequestExtractor(starlette_request)

assert extractor.is_json()
assert await extractor.json() == BODY_JSON


@pytest.mark.asyncio
async def test_starletterequestextractor_form(sentry_init):
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"],
]
# TODO add test for content-type: "application/x-www-form-urlencoded"

starlette_request = starlette.requests.Request(scope)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in FORM_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)

extractor = StarletteRequestExtractor(starlette_request)

form_data = await extractor.form()
assert form_data.keys() == PARSED_FORM.keys()
assert form_data["username"] == PARSED_FORM["username"]
assert form_data["password"] == PARSED_FORM["password"]
assert form_data["photo"].filename == PARSED_FORM["photo"].filename

# Make sure we still can read the body
# after alreading it with extractor.form() above.
body = await extractor.request.body()
assert body


@pytest.mark.asyncio
async def test_starletterequestextractor_body_consumed_twice(
sentry_init, capture_events
):
"""
Starlette does cache when you read the request data via `request.json()`
or `request.body()`, but it does NOT when using `request.form()`.
So we have an edge case when the Sentry Starlette reads the body using `.form()`
and the user wants to read the body using `.body()`.
Because the underlying stream can not be consumed twice and is not cached.

We have fixed this in `StarletteRequestExtractor.form()` by consuming the body
first with `.body()` (to put it into the `_body` cache and then consume it with `.form()`.

If this behavior is changed in Starlette and the `request.form()` in Starlette
is also caching the body, this test will fail.

See also https://github.com/encode/starlette/discussions/1933
"""
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"],
]

starlette_request = starlette.requests.Request(scope)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in FORM_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)

extractor = StarletteRequestExtractor(starlette_request)

await extractor.request.form()

with pytest.raises(RuntimeError):
await extractor.request.body()
assert transaction_event["request"]["cookies"] == {
"tasty_cookie": "strawberry",
"yummy_cookie": "choco",
}
assert transaction_event["request"]["data"] == BODY_JSON


@pytest.mark.asyncio
async def test_starletterequestextractor_extract_request_info_too_big(sentry_init):
async def test_formdata_request_body(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
max_request_body_size="always",
integrations=[StarletteIntegration()],
)
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"multipart/form-data; boundary=fd721ef49ea403a6"],
[b"content-length", str(len(BODY_FORM)).encode()],
[b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"],
]
starlette_request = starlette.requests.Request(scope)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in FORM_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)
starlette_app = starlette_app_factory()
events = capture_events()

extractor = StarletteRequestExtractor(starlette_request)
client = TestClient(starlette_app)
client.post(
"/body/form",
data=BODY_FORM.encode("utf-8"),
headers={
"content-type": "multipart/form-data; boundary=fd721ef49ea403a6",
},
)

request_info = await extractor.extract_request_info()
(event, transaction_event) = events
assert event["request"]["data"].keys() == PARSED_FORM.keys()
assert event["request"]["data"]["username"] == PARSED_FORM["username"]
assert event["request"]["data"]["password"] == "[Filtered]"
assert event["request"]["data"]["photo"] == ""
assert transaction_event["_meta"]["request"]["data"]["photo"] == {
"": {"rem": [["!raw", "x"]]}
}

assert request_info
assert request_info["cookies"] == {
"tasty_cookie": "strawberry",
"yummy_cookie": "choco",
assert transaction_event["request"]["data"].keys() == PARSED_FORM.keys()
assert transaction_event["request"]["data"]["username"] == PARSED_FORM["username"]
assert transaction_event["request"]["data"]["password"] == "[Filtered]"
assert transaction_event["request"]["data"]["photo"] == ""
assert transaction_event["_meta"]["request"]["data"]["photo"] == {
"": {"rem": [["!raw", "x"]]}
}
# Because request is too big only the AnnotatedValue is extracted.
assert request_info["data"].metadata == {"rem": [["!config", "x"]]}


@pytest.mark.asyncio
async def test_starletterequestextractor_extract_request_info(sentry_init):
async def test_request_body_too_big(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[StarletteIntegration()],
)
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"application/json"],
[b"content-length", str(len(json.dumps(BODY_JSON))).encode()],
[b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"],
]

starlette_request = starlette.requests.Request(scope)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)
starlette_app = starlette_app_factory()
events = capture_events()

extractor = StarletteRequestExtractor(starlette_request)
client = TestClient(starlette_app)
client.post(
"/body/form",
data=BODY_FORM.encode("utf-8"),
headers={
"content-type": "multipart/form-data; boundary=fd721ef49ea403a6",
"cookie": "yummy_cookie=choco; tasty_cookie=strawberry",
},
)

request_info = await extractor.extract_request_info()
(event, transaction_event) = events
assert event["request"]["cookies"] == {
"tasty_cookie": "strawberry",
"yummy_cookie": "choco",
}
# Because request is too big only the AnnotatedValue is extracted.
assert event["_meta"]["request"]["data"] == {"": {"rem": [["!config", "x"]]}}

assert request_info
assert request_info["cookies"] == {
assert transaction_event["request"]["cookies"] == {
"tasty_cookie": "strawberry",
"yummy_cookie": "choco",
}
assert request_info["data"] == BODY_JSON
# Because request is too big only the AnnotatedValue is extracted.
assert transaction_event["_meta"]["request"]["data"] == {
"": {"rem": [["!config", "x"]]}
}


@pytest.mark.asyncio
async def test_starletterequestextractor_extract_request_info_no_pii(sentry_init):
async def test_request_info_no_pii(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=False,
integrations=[StarletteIntegration()],
)
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"application/json"],
[b"content-length", str(len(json.dumps(BODY_JSON))).encode()],
[b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"],
]

starlette_request = starlette.requests.Request(scope)

# Mocking async `_receive()` that works in Python 3.7+
side_effect = [_mock_receive(msg) for msg in JSON_RECEIVE_MESSAGES]
starlette_request._receive = mock.Mock(side_effect=side_effect)
starlette_app = starlette_app_factory()
events = capture_events()

extractor = StarletteRequestExtractor(starlette_request)
client = TestClient(starlette_app)
client.post(
"/body/json",
json=BODY_JSON,
headers={
"cookie": "yummy_cookie=choco; tasty_cookie=strawberry",
},
)

request_info = await extractor.extract_request_info()
(event, transaction_event) = events
assert "cookies" not in event["request"]
assert event["request"]["data"] == BODY_JSON

assert request_info
assert "cookies" not in request_info
assert request_info["data"] == BODY_JSON
assert "cookies" not in transaction_event["request"]
assert transaction_event["request"]["data"] == BODY_JSON


@pytest.mark.parametrize(
Expand Down Expand Up @@ -1624,25 +1589,23 @@ async def _error(_):


@pytest.mark.asyncio
async def test_starletterequestextractor_malformed_json_error_handling(sentry_init):
scope = SCOPE.copy()
scope["headers"] = [
[b"content-type", b"application/json"],
]
starlette_request = starlette.requests.Request(scope)

malformed_json = "{invalid json"
malformed_messages = [
{"type": "http.request", "body": malformed_json.encode("utf-8")},
{"type": "http.disconnect"},
]

side_effect = [_mock_receive(msg) for msg in malformed_messages]
starlette_request._receive = mock.Mock(side_effect=side_effect)
async def test_malformed_json_request_body(sentry_init, capture_events):
sentry_init(
traces_sample_rate=1.0,
send_default_pii=True,
integrations=[StarletteIntegration()],
)

extractor = StarletteRequestExtractor(starlette_request)
starlette_app = starlette_app_factory()
events = capture_events()

assert extractor.is_json()
client = TestClient(starlette_app)
client.post(
"/body/raw",
data="{invalid json".encode("utf-8"),
headers={"content-type": "application/json"},
)

result = await extractor.json()
assert result is None
(event, transaction_event) = events
assert event["request"]["data"] == ""
assert transaction_event["request"]["data"] == ""
Loading