Skip to content

Commit 299f2de

Browse files
committed
test(fastapi): added tests for custom api route implementation fix
This commit adds tests that illustrate the original issue that was being experienced for custom api route implementations when they depended on non-standard fields existing on the ASGI HTTP connection scope. Before the fix was implemented, the inclusion of a custom API route in the FastAPI application would cause an exception to be raised inside the OpenTelemetryMiddleware since the non-standard fields do not exist on the ASGI HTTP connection scope until after the subsequent middleware runs and adds the expected fields.
1 parent de946d3 commit 299f2de

File tree

1 file changed

+44
-3
lines changed

1 file changed

+44
-3
lines changed

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import fastapi
2323
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
2424
from fastapi.responses import JSONResponse
25+
from fastapi.routing import APIRoute
2526
from fastapi.testclient import TestClient
27+
from starlette.routing import Match
28+
from starlette.types import Receive, Scope, Send
2629

2730
import opentelemetry.instrumentation.fastapi as otel_fastapi
2831
from opentelemetry import trace
@@ -38,9 +41,7 @@
3841
from 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
4445
from opentelemetry.sdk.metrics.export import (
4546
HistogramDataPoint,
4647
NumberDataPoint,
@@ -123,6 +124,23 @@
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+
126144
class 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

308342
class 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

Comments
 (0)