Skip to content

[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

@biyani701

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions