Skip to content

Commit 7277501

Browse files
committed
fix: call post_init_error within init exception handling block
Restores sys.exc_info() access for tools that monkey-patch post_init_error (e.g. Sentry SDK). The init error paths are merged into a single except block that type-checks FaultException, keeping error reporting behavior identical for both paths. Fixes #172
1 parent 46e4c6a commit 7277501

2 files changed

Lines changed: 68 additions & 11 deletions

File tree

awslambdaric/bootstrap.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -495,29 +495,31 @@ def run(handler, lambda_runtime_client):
495495
sys.stderr = Unbuffered(sys.stderr)
496496

497497
with create_log_sink() as log_sink:
498-
error_result = None
499-
500498
try:
501499
_setup_logging(_AWS_LAMBDA_LOG_FORMAT, _AWS_LAMBDA_LOG_LEVEL, log_sink)
502500
global _GLOBAL_AWS_REQUEST_ID, _GLOBAL_TENANT_ID
503501

504502
_log_preview_runtime_warning()
505503

506504
request_handler = _get_handler(handler)
507-
except FaultException as e:
508-
error_result = make_error(
509-
e.msg,
510-
e.exception_type,
511-
e.trace,
512-
)
513-
except Exception:
514-
error_result = build_fault_result(sys.exc_info(), None)
505+
except Exception as e:
506+
if isinstance(e, FaultException):
507+
error_result = make_error(
508+
e.msg,
509+
e.exception_type,
510+
e.trace,
511+
)
512+
else:
513+
error_result = build_fault_result(sys.exc_info(), None)
515514

516-
if error_result is not None:
517515
from .lambda_literals import lambda_unhandled_exception_warning_message
518516

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

523525
sys.exit(1)

tests/test_bootstrap.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging.config
99
import os
1010
import re
11+
import sys
1112
import tempfile
1213
import time
1314
import traceback
@@ -1536,6 +1537,60 @@ class TestException(Exception):
15361537

15371538
mock_sys.exit.assert_called_once_with(1)
15381539

1540+
def test_run_fault_exception_post_init_error_within_exception_context(self):
1541+
# Regression test for GitHub issue #172: post_init_error must be
1542+
# invoked while the init error is still being handled so that
1543+
# monkey-patched implementations (e.g. Sentry SDK) can access the
1544+
# exception through sys.exc_info().
1545+
expected_handler = "app.my_test_handler"
1546+
1547+
captured_exc_info = []
1548+
1549+
def capture_exc_info(*args, **kwargs):
1550+
captured_exc_info.append(sys.exc_info())
1551+
1552+
mock_runtime_client = MagicMock()
1553+
mock_runtime_client.post_init_error.side_effect = capture_exc_info
1554+
1555+
with self.assertRaises(SystemExit) as cm:
1556+
bootstrap.run(expected_handler, mock_runtime_client)
1557+
1558+
self.assertEqual(cm.exception.code, 1)
1559+
mock_runtime_client.post_init_error.assert_called_once()
1560+
1561+
etype, value, tb = captured_exc_info[0]
1562+
self.assertIs(etype, FaultException)
1563+
self.assertIsNotNone(value)
1564+
self.assertIsNotNone(tb)
1565+
1566+
@patch(
1567+
"awslambdaric.bootstrap.LambdaLoggerHandler",
1568+
Mock(side_effect=Exception("Boom!")),
1569+
)
1570+
@patch("awslambdaric.bootstrap.log_error", MagicMock())
1571+
def test_run_generic_exception_post_init_error_within_exception_context(self):
1572+
# Same as above, but for the non-FaultException init error path.
1573+
expected_handler = "app.my_test_handler"
1574+
1575+
captured_exc_info = []
1576+
1577+
def capture_exc_info(*args, **kwargs):
1578+
captured_exc_info.append(sys.exc_info())
1579+
1580+
mock_runtime_client = MagicMock()
1581+
mock_runtime_client.post_init_error.side_effect = capture_exc_info
1582+
1583+
with self.assertRaises(SystemExit) as cm:
1584+
bootstrap.run(expected_handler, mock_runtime_client)
1585+
1586+
self.assertEqual(cm.exception.code, 1)
1587+
mock_runtime_client.post_init_error.assert_called_once()
1588+
1589+
etype, value, tb = captured_exc_info[0]
1590+
self.assertIs(etype, Exception)
1591+
self.assertEqual(str(value), "Boom!")
1592+
self.assertIsNotNone(tb)
1593+
15391594

15401595
class TestOnInitComplete(unittest.TestCase):
15411596
def tearDown(self):

0 commit comments

Comments
 (0)