2020import weakref as _weakref
2121from contextlib import ExitStack
2222from timeit import default_timer
23- from typing import Any , cast
23+ from typing import Any , Final , cast
2424from unittest .mock import Mock , call , patch
2525
2626import fastapi
2727import pytest
2828from fastapi .middleware .httpsredirect import HTTPSRedirectMiddleware
2929from fastapi .responses import JSONResponse , PlainTextResponse
30+ from fastapi .routing import APIRoute
3031from fastapi .testclient import TestClient
32+ from starlette .routing import Match
33+ from starlette .types import Receive , Scope , Send
3134
3235import opentelemetry .instrumentation .fastapi as otel_fastapi
3336from opentelemetry import trace
131134)
132135
133136
137+ class CustomMiddleware :
138+ def __init__ (self , app : fastapi .FastAPI ) -> None :
139+ self .app = app
140+
141+ async def __call__ (
142+ self , scope : Scope , receive : Receive , send : Send
143+ ) -> None :
144+ scope ["nonstandard_field" ] = "here"
145+ await self .app (scope , receive , send )
146+
147+
148+ class CustomRoute (APIRoute ):
149+ def matches (self , scope : Scope ) -> tuple [Match , Scope ]:
150+ assert "nonstandard_field" in scope
151+ return super ().matches (scope )
152+
153+
134154class TestBaseFastAPI (TestBase ):
135155 def _create_app (self ):
136156 app = self ._create_fastapi_app ()
@@ -191,6 +211,7 @@ def setUp(self):
191211 self ._instrumentor = otel_fastapi .FastAPIInstrumentor ()
192212 self ._app = self ._create_app ()
193213 self ._app .add_middleware (HTTPSRedirectMiddleware )
214+ self ._app .add_middleware (CustomMiddleware )
194215 self ._client = TestClient (self ._app , base_url = "https://testserver:443" )
195216 # run the lifespan, initialize the middleware stack
196217 # this is more in-line with what happens in a real application when the server starts up
@@ -210,6 +231,7 @@ def tearDown(self):
210231 def _create_fastapi_app ():
211232 app = fastapi .FastAPI ()
212233 sub_app = fastapi .FastAPI ()
234+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
213235
214236 @sub_app .get ("/home" )
215237 async def _ ():
@@ -235,6 +257,12 @@ async def _():
235257 async def _ ():
236258 raise UnhandledException ("This is an unhandled exception" )
237259
260+ @custom_router .get ("/success" )
261+ async def _ ():
262+ return None
263+
264+ app .include_router (custom_router , prefix = "/custom-router" )
265+
238266 app .mount ("/sub" , app = sub_app )
239267 app .host ("testserver2" , sub_app )
240268
@@ -313,6 +341,28 @@ def test_sub_app_fastapi_call(self):
313341 span .attributes [HTTP_URL ],
314342 )
315343
344+ def test_custom_api_router (self ):
345+ """
346+ This test is to ensure that custom API routers the OpenTelemetryMiddleware does not cause issues with
347+ custom API routers that depend on non-standard fields on the ASGI scope.
348+ """
349+ resp : Final = self ._client .get ("/custom-router/success" )
350+ spans : Final = self .memory_exporter .get_finished_spans ()
351+ spans_with_http_attributes = [
352+ span
353+ for span in spans
354+ if (HTTP_URL in span .attributes or HTTP_TARGET in span .attributes )
355+ ]
356+ self .assertEqual (200 , resp .status_code )
357+ for span in spans_with_http_attributes :
358+ self .assertEqual (
359+ "/custom-router/success" , span .attributes [HTTP_TARGET ]
360+ )
361+ self .assertEqual (
362+ "https://testserver/custom-router/success" ,
363+ span .attributes [HTTP_URL ],
364+ )
365+
316366 def test_host_fastapi_call (self ):
317367 client = TestClient (self ._app , base_url = "https://testserver2:443" )
318368 client .get ("/" )
@@ -1017,6 +1067,7 @@ def test_metric_uninstrument(self):
10171067 def _create_fastapi_app ():
10181068 app = fastapi .FastAPI ()
10191069 sub_app = fastapi .FastAPI ()
1070+ custom_router = fastapi .APIRouter (route_class = CustomRoute )
10201071
10211072 @sub_app .get ("/home" )
10221073 async def _ ():
@@ -1042,6 +1093,12 @@ async def _():
10421093 async def _ ():
10431094 raise UnhandledException ("This is an unhandled exception" )
10441095
1096+ @custom_router .get ("/success" )
1097+ async def _ ():
1098+ return None
1099+
1100+ app .include_router (custom_router , prefix = "/custom-router" )
1101+
10451102 app .mount ("/sub" , app = sub_app )
10461103
10471104 return app
0 commit comments