diff --git a/awslambdaric/bootstrap.py b/awslambdaric/bootstrap.py index f54ca1a..51ed490 100644 --- a/awslambdaric/bootstrap.py +++ b/awslambdaric/bootstrap.py @@ -495,8 +495,6 @@ 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 @@ -504,20 +502,24 @@ def run(handler, lambda_runtime_client): _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) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 1eb2bb0..aa24788 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -8,6 +8,7 @@ import logging.config import os import re +import sys import tempfile import time import traceback @@ -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):