Skip to content

[BUG] Dash.run(debug=True, backend="fastapi") reload subprocess silently ignores reload_dirs/reload_excludes kwargs #3962

Description

@biyani701

Summary

When using the FastAPI backend, app.run(host=..., port=..., debug=True, **kwargs) (main-thread, non-Jupyter path) triggers dash/backends/_fastapi.py::FastAPIBackend.run(), which spawns python -m uvicorn <module>:<app>.server --reload as a subprocess. Extra kwargs such as reload_dirs=[...] or reload_excludes=[...] are accepted by Dash.run(..., **flask_run_options) without error and flow straight into FastAPIBackend.run()'s **kwargs — but only kwargs.get("reload") is ever read there; every other reload-related kwarg is silently dropped when building the subprocess's CLI args. The comment directly above the subprocess.Popen(...) call even says:

# Add any other kwargs as CLI args if needed
proc = subprocess.Popen(uvicorn_args, env=env)

...but nothing implements it.

Practical effect: without an explicit --reload-dir, uvicorn/watchfiles fall back to watching the entire current working directory. In any project run with cwd above the app's own package (e.g. a monorepo, or simply python path/to/app/index.py invoked from a parent directory), the reload subprocess ends up recursively watching unrelated directories — including a shared virtualenv (.venv/site-packages/...) — so any unrelated package file being touched triggers a full, spurious app reload.

Reproduction

# repro.py -- run as `python repro.py` from a directory ABOVE this file,
# e.g.:
#   mkdir -p repo_root/app_dir && cd repo_root
#   (place this file at repo_root/app_dir/repro.py)
#   python app_dir/repro.py
from dash import Dash, html

app = Dash(__name__, backend="fastapi")
app.layout = html.Div("hello")

if __name__ == "__main__":
    app.run(
        host="127.0.0.1",
        port=8050,
        debug=True,
        reload_dirs=["app_dir"],
        reload_excludes=["app_dir/tests/*"],
    )
  • Expected: the spawned uvicorn --reload subprocess only watches app_dir/, excluding app_dir/tests/*.
  • Actual: reload_dirs/reload_excludes are silently ignored. The subprocess inherits the parent's cwd (repo_root/) and uvicorn defaults reload_dirs to [Path.cwd()], so it watches repo_root/ recursively -- including repo_root/.venv if one exists there.

Root cause

dash/backends/_fastapi.py, FastAPIBackend.run() (dash 4.4.1):

def run(self, dash_app: Dash, host, port, debug, **kwargs):
    ...
    uvicorn_args = [
        sys.executable, "-m", "uvicorn", app_path,
        "--host", str(host), "--port", str(port),
    ]
    if kwargs.get("reload"):
        uvicorn_args.append("--reload")

    dev_tools = dash_app._dev_tools
    config = dict(
        {"debug": debug} if debug else {"debug": False},
        **{f"dev_tools_{k}": v for k, v in dev_tools.items()},
    )
    env = os.environ.copy()
    env[_ENV_CONFIG] = json.dumps(config)

    # Add any other kwargs as CLI args if needed

    proc = subprocess.Popen(uvicorn_args, env=env)
    proc.wait()

Only kwargs["reload"] is ever consulted. reload_dirs, reload_excludes, reload_includes (and anything else a caller passes through app.run(...)) reach this function but are never translated into --reload-dir / --reload-exclude / --reload-include CLI args for the child process.

Suggested fix

Forward at least the reload-scoping kwargs onto uvicorn_args, e.g.:

for d in kwargs.get("reload_dirs") or []:
    uvicorn_args += ["--reload-dir", str(d)]
for p in kwargs.get("reload_excludes") or []:
    uvicorn_args += ["--reload-exclude", str(p)]
for p in kwargs.get("reload_includes") or []:
    uvicorn_args += ["--reload-include", str(p)]

Since this is silent today (no error, no warning), a caller has no way to discover the gap short of reading this source file directly. At minimum, forwarding these three would close the gap for the common case; more generally it might be worth warning on any kwarg that isn't recognized/forwarded here.

Environment

  • dash 4.4.1 (dash[fastapi], backend="fastapi")
  • uvicorn 0.52.0
  • Windows 11, Python 3.11 (the affected code path is not inside a platform-specific branch, so this should reproduce on any OS)

Related

Possibly adjacent to #3818 / #3912 (uvicorn --reload subprocess handling in the FastAPI backend also has callback double-registration issues there), but this is a distinct symptom -- silently dropped reload-scoping kwargs / overly broad default watch scope, not callback duplication.

Metadata

Metadata

Assignees

Labels

P2considered for next cyclebugsomething brokensize: 1

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions