2222import fastapi
2323from fastapi .middleware .httpsredirect import HTTPSRedirectMiddleware
2424from fastapi .responses import JSONResponse
25+ from fastapi .routing import APIRoute
2526from fastapi .testclient import TestClient
27+ from starlette .routing import Match
28+ from starlette .types import Receive , Scope , Send
2629
2730import opentelemetry .instrumentation .fastapi as otel_fastapi
2831from opentelemetry import trace
3841from opentelemetry .instrumentation .auto_instrumentation ._load import (
3942 _load_instrumentors ,
4043)
41- from opentelemetry .instrumentation .dependencies import (
42- DependencyConflict ,
43- )
44+ from opentelemetry .instrumentation .dependencies import DependencyConflict
4445from opentelemetry .sdk .metrics .export import (
4546 HistogramDataPoint ,
4647 NumberDataPoint ,
123124)
124125
125126
127+ class CustomMiddleware :
128+ def __init__ (self , app : fastapi .FastAPI ) -> None :
129+ self .app = app
130+
131+ async def __call__ (
132+ self , scope : Scope , receive : Receive , send : Send
133+ ) -> None :
134+ scope ["nonstandard_field" ] = "here"
135+ await self .app (scope , receive , send )
136+
137+
138+ class CustomRoute (APIRoute ):
139+ def matches (self , scope : Scope ) -> tuple [Match , Scope ]:
140+ assert "nonstandard_field" in scope
141+ return super ().matches (scope )
142+
143+
126144class TestBaseFastAPI (TestBase ):
127145 def _create_app (self ):
128146 app = self ._create_fastapi_app ()
@@ -183,6 +201,7 @@ def setUp(self):
183201 self ._instrumentor = otel_fastapi .FastAPIInstrumentor ()
184202 self ._app = self ._create_app ()
185203 self ._app .add_middleware (HTTPSRedirectMiddleware )
204+ self ._app .add_middleware (CustomMiddleware )
186205 self ._client = TestClient (self ._app , base_url = "https://testserver:443" )
187206 # run the lifespan, initialize the middleware stack
188207 # this is more in-line with what happens in a real application when the server starts up
@@ -202,6 +221,7 @@ def tearDown(self):
202221 def _create_fastapi_app ():
203222 app = fastapi .FastAPI ()
204223 sub_app = fastapi .FastAPI ()
224+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
205225
206226 @sub_app .get ("/home" )
207227 async def _ ():
@@ -227,6 +247,12 @@ async def _():
227247 async def _ ():
228248 raise UnhandledException ("This is an unhandled exception" )
229249
250+ @custom_router .get ("/success" )
251+ async def _ ():
252+ return None
253+
254+ app .include_router (custom_router , prefix = "/custom-router" )
255+
230256 app .mount ("/sub" , app = sub_app )
231257
232258 return app
@@ -304,6 +330,14 @@ def test_sub_app_fastapi_call(self):
304330 span .attributes [HTTP_URL ],
305331 )
306332
333+ def test_custom_api_router (self ):
334+ """
335+ This test is to ensure that custom API routers the OpenTelemetryMiddleware does not cause issues with
336+ custom API routers that depend on non-standard fields on the ASGI scope.
337+ """
338+ resp = self ._client .get ("/custom-router/success" )
339+ self .assertEqual (resp .status_code , 200 )
340+
307341
308342class TestBaseAutoFastAPI (TestBaseFastAPI ):
309343 @classmethod
@@ -988,6 +1022,7 @@ def test_metric_uninstrument(self):
9881022 def _create_fastapi_app ():
9891023 app = fastapi .FastAPI ()
9901024 sub_app = fastapi .FastAPI ()
1025+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
9911026
9921027 @sub_app .get ("/home" )
9931028 async def _ ():
@@ -1013,6 +1048,12 @@ async def _():
10131048 async def _ ():
10141049 raise UnhandledException ("This is an unhandled exception" )
10151050
1051+ @custom_router .get ("/success" )
1052+ async def _ ():
1053+ return None
1054+
1055+ app .include_router (custom_router , prefix = "/custom-router" )
1056+
10161057 app .mount ("/sub" , app = sub_app )
10171058
10181059 return app
0 commit comments