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 supportifmodule.startswith("fastapi"):
app.run(**options)
# Quart support (ASGI - runs its own async event loop)elifmodule.startswith("quart"):
app.run(**options)
# Flask fallback (WSGI - needs threaded mode)else:
app.run(threaded=True, **options)This breaks for any fastapi.FastAPIsubclass 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):
importfastapifromdashimportDash, htmlfromdash.testing.application_runnersimportThreadedRunnerclassWrappedFastAPI(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 DashAppLoadingErrorprint("started:", runner.started, runner.url)
runner.stop()Real-world trigger, for reference:
fromfastapiimportFastAPIfromopentelemetry.instrumentation.fastapiimportFastAPIInstrumentorfromdashimportDashFastAPIInstrumentor().instrument() # swaps fastapi.FastAPI -> _InstrumentedFastAPIserver=FastAPI() # now actually an _InstrumentedFastAPI instanceapp=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:
importfastapias_fastapiexceptImportError:
_fastapi=Nonetry:
importquartas_quartexceptImportError:
_quart=Noneif_fastapiisnotNoneandisinstance(app.server, _fastapi.FastAPI):
app.run(**options)
elif_quartisnotNoneandisinstance(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.
fromopentelemetry.instrumentation.fastapiimport_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.