Repository navigation
[BUG] dash.testing.ThreadedRunner misdetects a subclassed/wrapped FastAPI app as Flask (e.g. under OpenTelemetry auto-instrumentation) -> TypeError: Config.__init__() got an unexpected keyword argument 'threaded' #3942
Description
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 failsExpected 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.
Activity
camdecoster commentedon Aug 5, 2026
Thanks for reporting this! Our team doesn't have time to work on this at the moment, but we could prioritize a PR review if you'd like to fix it.