Skip to content
Open
Show file tree
Hide file tree
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
24 changes: 13 additions & 11 deletions awslambdaric/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,29 +495,31 @@ def run(handler, lambda_runtime_client):
sys.stderr = Unbuffered(sys.stderr)

with create_log_sink() as log_sink:
error_result = None

try:
_setup_logging(_AWS_LAMBDA_LOG_FORMAT, _AWS_LAMBDA_LOG_LEVEL, log_sink)
global _GLOBAL_AWS_REQUEST_ID, _GLOBAL_TENANT_ID

_log_preview_runtime_warning()

request_handler = _get_handler(handler)
except FaultException as e:
error_result = make_error(
e.msg,
e.exception_type,
e.trace,
)
except Exception:
error_result = build_fault_result(sys.exc_info(), None)
except Exception as e:
if isinstance(e, FaultException):
error_result = make_error(
e.msg,
e.exception_type,
e.trace,
)
else:
error_result = build_fault_result(sys.exc_info(), None)

if error_result is not None:
from .lambda_literals import lambda_unhandled_exception_warning_message

logging.warning(lambda_unhandled_exception_warning_message)
log_error(error_result, log_sink)
# post_init_error must be called within the exception handling block
# so that the init error being handled is still accessible via
# sys.exc_info() (e.g. APM tools such as Sentry monkey-patch
# post_init_error and rely on this). See issue #172.
lambda_runtime_client.post_init_error(error_result)

sys.exit(1)
Expand Down
55 changes: 55 additions & 0 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging.config
import os
import re
import sys
import tempfile
import time
import traceback
Expand Down Expand Up @@ -1536,6 +1537,60 @@ class TestException(Exception):

mock_sys.exit.assert_called_once_with(1)

def test_run_fault_exception_post_init_error_within_exception_context(self):
# Regression test for GitHub issue #172: post_init_error must be
# invoked while the init error is still being handled so that
# monkey-patched implementations (e.g. Sentry SDK) can access the
# exception through sys.exc_info().
expected_handler = "app.my_test_handler"

captured_exc_info = []

def capture_exc_info(*args, **kwargs):
captured_exc_info.append(sys.exc_info())

mock_runtime_client = MagicMock()
mock_runtime_client.post_init_error.side_effect = capture_exc_info

with self.assertRaises(SystemExit) as cm:
bootstrap.run(expected_handler, mock_runtime_client)

self.assertEqual(cm.exception.code, 1)
mock_runtime_client.post_init_error.assert_called_once()

etype, value, tb = captured_exc_info[0]
self.assertIs(etype, FaultException)
self.assertIsNotNone(value)
self.assertIsNotNone(tb)

@patch(
"awslambdaric.bootstrap.LambdaLoggerHandler",
Mock(side_effect=Exception("Boom!")),
)
@patch("awslambdaric.bootstrap.log_error", MagicMock())
def test_run_generic_exception_post_init_error_within_exception_context(self):
# Same as above, but for the non-FaultException init error path.
expected_handler = "app.my_test_handler"

captured_exc_info = []

def capture_exc_info(*args, **kwargs):
captured_exc_info.append(sys.exc_info())

mock_runtime_client = MagicMock()
mock_runtime_client.post_init_error.side_effect = capture_exc_info

with self.assertRaises(SystemExit) as cm:
bootstrap.run(expected_handler, mock_runtime_client)

self.assertEqual(cm.exception.code, 1)
mock_runtime_client.post_init_error.assert_called_once()

etype, value, tb = captured_exc_info[0]
self.assertIs(etype, Exception)
self.assertEqual(str(value), "Boom!")
self.assertIsNotNone(tb)


class TestOnInitComplete(unittest.TestCase):
def tearDown(self):
Expand Down
Loading