Thank you so much for helping improve the quality of Dash!
Describe your context
dash 4.4.0
fastapi 0.136.3
uvicorn 0.49.0
Also reproduced against dash 4.4.1 (latest release as of this report) — the relevant code in dash/testing/application_runners.py is unchanged between 4.4.0 and 4.4.1.
- OS: Windows 11
- Python: 3.14.5
- Not frontend-related (pure
dash.testing / backend issue)
Describe the bug
dash.testing.application_runners.ThreadedRunner.start() picks the FastAPI/Quart vs. Flask app.run() branch by string-sniffing the server's class module:
module = app.server.__class__.__module__
# FastAPI support
if module.startswith("fastapi"):
app.run(**options)
# Quart support (ASGI - runs its own async event loop)
elif module.startswith("quart"):
app.run(**options)
# Flask fallback (WSGI - needs threaded mode)
else:
app.run(threaded=True, **options)
This breaks for any fastapi.FastAPI subclass defined outside a module literally named fastapi.* — which is exactly what several common FastAPI instrumentation/wrapping libraries do, e.g. opentelemetry-instrumentation-fastapi's FastAPIInstrumentor, which replaces fastapi.FastAPI process-wide with opentelemetry.instrumentation.fastapi._InstrumentedFastAPI (a FastAPI subclass whose __module__ is "opentelemetry.instrumentation.fastapi", not "fastapi.*").
When that happens, ThreadedRunner falls through to the Flask branch and calls app.run(threaded=True, ...). For the FastAPI backend, that threaded kwarg flows straight into uvicorn.Config(...) (dash/backends/_fastapi.py), which doesn't accept it:
TypeError: Config.__init__() got an unexpected keyword argument 'threaded'
...which ThreadedRunner then reports as DashAppLoadingError: threaded server failed to start. This makes dash[testing] (and anything built on dash_duo/dash_thread_server, e.g. Playwright-based test suites using dash.testing.plugin) unusable for any FastAPI-backend app that has OpenTelemetry auto-instrumentation (or any other subclassing wrapper) applied to it.
Minimal reproduction (no OpenTelemetry install required — just simulates the same "FastAPI subclass defined in another module" shape):
import fastapi
from dash import Dash, html
from dash.testing.application_runners import ThreadedRunner
class WrappedFastAPI(fastapi.FastAPI):
"""Stands in for any third-party FastAPI subclass defined in another
module -- e.g. opentelemetry.instrumentation.fastapi._InstrumentedFastAPI."""
WrappedFastAPI.__module__ = "some_other_package.wrapper"
server = WrappedFastAPI()
app = Dash(__name__, server=server)
app.layout = html.Div("hello")
runner = ThreadedRunner()
runner.start(app, start_timeout=5) # raises DashAppLoadingError
print("started:", runner.started, runner.url)
runner.stop()
Real-world trigger, for reference:
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from dash import Dash
FastAPIInstrumentor().instrument() # swaps fastapi.FastAPI -> _InstrumentedFastAPI
server = FastAPI() # now actually an _InstrumentedFastAPI instance
app = Dash(__name__, server=server)
# any dash.testing fixture that goes through ThreadedRunner now fails
Expected behavior
ThreadedRunner should recognize a FastAPI (or Quart) app regardless of what module the concrete class is defined in — e.g. via isinstance(app.server, fastapi.FastAPI) / isinstance(app.server, quart.Quart) instead of a __module__ string prefix check. isinstance correctly handles subclasses defined anywhere, which a module-name string check fundamentally cannot.
Suggested fix in dash/testing/application_runners.py::ThreadedRunner.start():
try:
import fastapi as _fastapi
except ImportError:
_fastapi = None
try:
import quart as _quart
except ImportError:
_quart = None
if _fastapi is not None and isinstance(app.server, _fastapi.FastAPI):
app.run(**options)
elif _quart is not None and isinstance(app.server, _quart.Quart):
app.run(**options)
else:
app.run(threaded=True, **options)
(MultiProcessRunner.start() in the same file has the identical pattern and would benefit from the same fix.)
Workaround we're using in the meantime (test-fixture-only, not a real fix): retag the instrumented class's __module__ before starting the server, e.g.
from opentelemetry.instrumentation.fastapi import _InstrumentedFastAPI
_InstrumentedFastAPI.__module__ = "fastapi.instrumented"
...but this is fragile (breaks if OTel's internal class name changes, and mutates third-party class state) and doesn't help anyone not already deep in dash.testing internals — hence this report.
Thank you so much for helping improve the quality of Dash!
Describe your context
Also reproduced against dash 4.4.1 (latest release as of this report) — the relevant code in
dash/testing/application_runners.pyis unchanged between 4.4.0 and 4.4.1.dash.testing/ backend issue)Describe the bug
dash.testing.application_runners.ThreadedRunner.start()picks the FastAPI/Quart vs. Flaskapp.run()branch by string-sniffing the server's class module:This breaks for any
fastapi.FastAPIsubclass defined outside a module literally namedfastapi.*— which is exactly what several common FastAPI instrumentation/wrapping libraries do, e.g.opentelemetry-instrumentation-fastapi'sFastAPIInstrumentor, which replacesfastapi.FastAPIprocess-wide withopentelemetry.instrumentation.fastapi._InstrumentedFastAPI(aFastAPIsubclass whose__module__is"opentelemetry.instrumentation.fastapi", not"fastapi.*").When that happens,
ThreadedRunnerfalls through to the Flask branch and callsapp.run(threaded=True, ...). For the FastAPI backend, thatthreadedkwarg flows straight intouvicorn.Config(...)(dash/backends/_fastapi.py), which doesn't accept it:...which
ThreadedRunnerthen reports asDashAppLoadingError: threaded server failed to start. This makesdash[testing](and anything built ondash_duo/dash_thread_server, e.g. Playwright-based test suites usingdash.testing.plugin) unusable for any FastAPI-backend app that has OpenTelemetry auto-instrumentation (or any other subclassing wrapper) applied to it.Minimal reproduction (no OpenTelemetry install required — just simulates the same "FastAPI subclass defined in another module" shape):
Real-world trigger, for reference:
Expected behavior
ThreadedRunnershould recognize a FastAPI (or Quart) app regardless of what module the concrete class is defined in — e.g. viaisinstance(app.server, fastapi.FastAPI)/isinstance(app.server, quart.Quart)instead of a__module__string prefix check.isinstancecorrectly handles subclasses defined anywhere, which a module-name string check fundamentally cannot.Suggested fix in
dash/testing/application_runners.py::ThreadedRunner.start():(
MultiProcessRunner.start()in the same file has the identical pattern and would benefit from the same fix.)Workaround we're using in the meantime (test-fixture-only, not a real fix): retag the instrumented class's
__module__before starting the server, e.g....but this is fragile (breaks if OTel's internal class name changes, and mutates third-party class state) and doesn't help anyone not already deep in
dash.testinginternals — hence this report.