@@ -1877,3 +1877,107 @@ def test_custom_header_not_present_in_non_recording_span(self):
18771877 self .assertEqual (200 , resp .status_code )
18781878 span_list = self .memory_exporter .get_finished_spans ()
18791879 self .assertEqual (len (span_list ), 0 )
1880+
1881+
1882+ class TestTraceableExceptionHandling (TestBase ):
1883+ """Tests to ensure FastAPI exception handlers are only executed once and with a valid context"""
1884+
1885+ def setUp (self ):
1886+ super ().setUp ()
1887+
1888+ self .app = fastapi .FastAPI ()
1889+
1890+ otel_fastapi .FastAPIInstrumentor ().instrument_app (self .app )
1891+ self .client = TestClient (self .app )
1892+ self .tracer = self .tracer_provider .get_tracer (__name__ )
1893+ self .executed = 0
1894+ self .request_trace_id = None
1895+ self .error_trace_id = None
1896+
1897+ def tearDown (self ) -> None :
1898+ super ().tearDown ()
1899+ with self .disable_logging ():
1900+ otel_fastapi .FastAPIInstrumentor ().uninstrument_app (self .app )
1901+
1902+ def test_error_handler_context (self ):
1903+ """OTEL tracing contexts must be available during error handler execution"""
1904+
1905+ @self .app .exception_handler (Exception )
1906+ async def _ (* _ ):
1907+ self .error_trace_id = (
1908+ trace .get_current_span ().get_span_context ().trace_id
1909+ )
1910+
1911+ @self .app .get ("/foobar" )
1912+ async def _ ():
1913+ self .request_trace_id = (
1914+ trace .get_current_span ().get_span_context ().trace_id
1915+ )
1916+ raise Exception ("Test Exception" )
1917+
1918+ try :
1919+ self .client .get (
1920+ "/foobar" ,
1921+ )
1922+ except Exception :
1923+ pass
1924+
1925+ self .assertIsNotNone (self .request_trace_id )
1926+ self .assertEqual (self .request_trace_id , self .error_trace_id )
1927+
1928+ def test_error_handler_side_effects (self ):
1929+ """FastAPI default exception handlers (aka error handlers) must be executed exactly once per exception"""
1930+
1931+ @self .app .exception_handler (Exception )
1932+ async def _ (* _ ):
1933+ self .executed += 1
1934+
1935+ @self .app .get ("/foobar" )
1936+ async def _ ():
1937+ raise Exception ("Test Exception" )
1938+
1939+ try :
1940+ self .client .get (
1941+ "/foobar" ,
1942+ )
1943+ except Exception :
1944+ pass
1945+
1946+ self .assertEqual (self .executed , 1 )
1947+
1948+
1949+ class TestFailsafeHooks (TestBase ):
1950+ """Tests to ensure FastAPI instrumentation hooks don't tear through"""
1951+
1952+ def setUp (self ):
1953+ super ().setUp ()
1954+
1955+ self .app = fastapi .FastAPI ()
1956+
1957+ @self .app .get ("/foobar" )
1958+ async def _ ():
1959+ return {"message" : "Hello World" }
1960+
1961+ def failing_hook (* _ ):
1962+ raise Exception ("Hook Exception" )
1963+
1964+ otel_fastapi .FastAPIInstrumentor ().instrument_app (
1965+ self .app ,
1966+ server_request_hook = failing_hook ,
1967+ client_request_hook = failing_hook ,
1968+ client_response_hook = failing_hook ,
1969+ )
1970+ self .client = TestClient (self .app )
1971+
1972+ def tearDown (self ) -> None :
1973+ super ().tearDown ()
1974+ with self .disable_logging ():
1975+ otel_fastapi .FastAPIInstrumentor ().uninstrument_app (self .app )
1976+
1977+ def test_failsafe_hooks (self ):
1978+ """Crashing hooks must not tear through"""
1979+ resp = self .client .get (
1980+ "/foobar" ,
1981+ )
1982+
1983+ self .assertEqual (200 , resp .status_code )
0 commit comments