Skip to content
Merged
91 changes: 74 additions & 17 deletions sentry_sdk/integrations/chalice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,8 +3,15 @@

import sentry_sdk
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.integrations.aws_lambda import _make_request_event_processor
from sentry_sdk.traces import (
SpanStatus,
StreamedSpan,
get_current_span,
)
from sentry_sdk.tracing import TransactionSource
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand DownExpand Up@@ -63,38 +70,88 @@
with sentry_sdk.isolation_scope() as scope:
with capture_internal_exceptions():
configured_time = app.lambda_context.get_remaining_time_in_millis()
scope.set_transaction_name(
app.lambda_context.function_name,
source=TransactionSource.COMPONENT,
)

scope.add_event_processor(
_make_request_event_processor(
app.current_request.to_dict(),
app.lambda_context,
configured_time,
)
)
try:
return view_function(**function_args)
except Exception as exc:
if isinstance(exc, ChaliceViewError):

if has_span_streaming_enabled(client.options):
current_span = get_current_span()
segment = None
if type(current_span) is StreamedSpan:
# A segment already exists (created by the AWS Lambda
# integration), so decorate it with Chalice attributes
# The AWS Lambda integration owns the span lifecycle
# (end + flush), but Chalice converts unhandled view exceptions
# into 500 responses, so the error must be captured here.
request_dict = app.current_request.to_dict()
headers = request_dict.get("headers", {})

header_attrs: "Dict[str, Any]" = {}
for header, value in _filter_headers(
headers, use_annotated_value=False
).items():
header_attrs[f"http.request.header.{header.lower()}"] = value
Comment on lines +91 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The Chalice integration can crash with an AttributeError if the incoming request has a "headers" key with a None value, as the code attempts to call .items() on it.
Severity: HIGH

Suggested Fix

Add a check to ensure headers is a dictionary before it's used. A pattern like if not isinstance(headers, dict): headers = {} should be added after retrieving the headers from request_dict, mirroring the safeguard present in the AWS Lambda integration.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: sentry_sdk/integrations/chalice.py#L91-L97
Potential issue: In the Chalice integration, headers are retrieved from the request
dictionary using `request_dict.get("headers", {})`. This does not guard against the case
where the `"headers"` key exists but its value is `None`. If `headers` is `None`, the
subsequent call to `_filter_headers(headers, ...).items()` will raise an
`AttributeError: 'NoneType' object has no attribute 'items'`, causing the application to
crash. This is a realistic scenario as Chalice uses AWS Lambda events, which can have
`None` as the value for headers, a possibility explicitly handled in the AWS Lambda
integration but missed here.


additional_attrs: "Dict[str, Any]" = {}
if "method" in request_dict:
additional_attrs["http.request.method"] = request_dict["method"]

attributes = {
"sentry.origin": ChaliceIntegration.origin,
**header_attrs,
**additional_attrs,
}

segment = current_span._segment

Check warning on line 109 in sentry_sdk/integrations/chalice.py

View check run for this annotation

@sentry/warden/ warden: find-bugs

Streaming attribute-setting code in Chalice view wrapper not guarded by capture_internal_exceptions()

In `_get_view_function_response`'s `wrapped_view_function`, the span-streaming setup block (the `if has_span_streaming_enabled(...)` branch, lines 81–110) runs outside `capture_internal_exceptions()`, which only wraps the `configured_time` and `scope.add_event_processor` calls (lines 71–78). Any SDK-internal exception raised while building header attributes (`_filter_headers`), accessing `current_span._segment`, or calling `segment.set_attributes(attributes)` will propagate out of the wrapped view function and crash the user's Chalice request. The `else` branch's `scope.set_transaction_name(...)` (line 130) is likewise unprotected; in the pre-change code the transaction-name setup was inside `capture_internal_exceptions()`. For example, if `request_dict.get('headers', {})` returns `None` (key present with a `None` value), `_filter_headers(headers, ...).items()` raises `AttributeError` with no SDK guard to suppress it.
segment.set_attributes(attributes)

try:
return view_function(**function_args)
except Exception as exc:
if isinstance(exc, ChaliceViewError):
raise
exc_info = sys.exc_info()
if segment:
segment.status = SpanStatus.ERROR.value
sentry_event, hint = event_from_exception(
exc_info,
Comment thread
sentry[bot] marked this conversation as resolved.
client_options=client.options,
mechanism={"type": "chalice", "handled": False},
)
sentry_sdk.capture_event(sentry_event, hint=hint)
if segment is None:
client.flush()
raise
exc_info = sys.exc_info()
event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "chalice", "handled": False},
else:
scope.set_transaction_name(
app.lambda_context.function_name,
source=TransactionSource.COMPONENT,
)
sentry_sdk.capture_event(event, hint=hint)
client.flush()
raise
try:
return view_function(**function_args)
except Exception as exc:
if isinstance(exc, ChaliceViewError):
raise
exc_info = sys.exc_info()
sentry_event, hint = event_from_exception(
exc_info,
client_options=client.options,
mechanism={"type": "chalice", "handled": False},
)
sentry_sdk.capture_event(sentry_event, hint=hint)
client.flush()
raise

return wrapped_view_function # type: ignore


class ChaliceIntegration(Integration):
identifier = "chalice"
origin = f"auto.function.{identifier}"

@staticmethod
def setup_once() -> None:
Expand Down
105 changes: 102 additions & 3 deletions tests/integrations/chalice/test_chalice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,11 +5,23 @@
from chalice.local import LambdaContext, LocalGateway
from pytest_chalice.handlers import RequestHandler

import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.integrations.chalice import CHALICE_VERSION, ChaliceIntegration
from sentry_sdk.utils import parse_version


def _populate_lambda_context(context):
fn = context.function_name
context.invoked_function_arn = (
f"arn:aws:lambda:us-east-1:123456789012:function:{fn}"
)
context.log_group_name = f"/aws/lambda/{fn}"
context.log_stream_name = "2024/01/01/[$LATEST]abcdef1234567890"
context.aws_request_id = "test-request-id-1234"
return context


def _generate_lambda_context(self):
# Monkeypatch of the function _generate_lambda_context
# from the class LocalGateway
Expand All@@ -19,11 +31,12 @@ def _generate_lambda_context(self):
timeout = 10 * 1000
else:
timeout = self._config.lambda_timeout * 1000
return LambdaContext(
context = LambdaContext(
function_name=self._config.function_name,
memory_size=self._config.lambda_memory_size,
max_runtime_ms=timeout,
)
return _populate_lambda_context(context)


@pytest.fixture
Expand DownExpand Up@@ -89,8 +102,8 @@ def test_scheduled_event(app, lambda_context_args):
def every_hour(event):
raise Exception("schedule event!")

context = LambdaContext(
*lambda_context_args, max_runtime_ms=10000, time_source=time
context = _populate_lambda_context(
LambdaContext(*lambda_context_args, max_runtime_ms=10000, time_source=time)
)

lambda_event = {
Expand DownExpand Up@@ -160,3 +173,89 @@ def test_transaction(
(event,) = events
assert event["transaction"] == expected_transaction
assert event["transaction_info"] == {"source": expected_source}


def _make_span_streaming_app(sentry_init):
sentry_init(
integrations=[ChaliceIntegration()],
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
app = Chalice(app_name="sentry_chalice")

@app.route("/message")
def hi():
capture_message("hi")
return {"status": "ok"}

@app.route("/boom")
def boom():
raise Exception("boom goes the dynamite!")

LocalGateway._generate_lambda_context = _generate_lambda_context

return app


def test_span_streaming_existing_span(
Comment thread
sentry-warden[bot] marked this conversation as resolved.
sentry_init,
capture_items,
):
"""When a segment already exists (e.g. created by the AWS Lambda
integration), Chalice decorates it instead of creating a duplicate."""
app = _make_span_streaming_app(sentry_init)
client = RequestHandler(app)
items = capture_items("span")

with sentry_sdk.traces.start_span(
name="lambda_segment",
parent_span=None,
attributes={
"sentry.origin": "auto.function.aws_lambda",
"sentry.op": "function.aws",
"faas.name": "api_handler",
},
):
response = client.get("/message")
assert response.status_code == 200

sentry_sdk.flush()

segment_spans = [s.payload for s in items if s.payload.get("is_segment")]
assert len(segment_spans) == 1
span = segment_spans[0]

attrs = span["attributes"]
assert attrs["sentry.origin"] == "auto.function.chalice"
assert attrs["sentry.op"] == "function.aws"
assert attrs["faas.name"] == "api_handler"
assert span["status"] == "ok"


def test_span_streaming_existing_span_error(
sentry_init,
capture_items,
):
app = _make_span_streaming_app(sentry_init)
client = RequestHandler(app)
items = capture_items("event", "span")

with sentry_sdk.traces.start_span(
name="lambda_segment",
parent_span=None,
attributes={"sentry.origin": "auto.function.aws_lambda"},
):
response = client.get("/boom")
assert response.status_code == 500

sentry_sdk.flush()
Comment thread
sentry-warden[bot] marked this conversation as resolved.

error_items = [i for i in items if i.type == "event"]
assert len(error_items) == 1

segment_spans = [
s.payload for s in items if s.type == "span" and s.payload.get("is_segment")
]
assert len(segment_spans) == 1
assert segment_spans[0]["attributes"]["sentry.origin"] == "auto.function.chalice"
assert segment_spans[0]["status"] == "error"
Loading