Describe your context
Please provide us your environment, so we can easily reproduce the issue.
- replace the result of
pip list | grep dash below
dash 4.2.0
dash-testing-stub 0.0.2
- Python 3.12 (Windows 10)
- Python 3.14 (Windows 10, Ubuntu).
Describe the bug
When a Dash app uses the Quart backend (Dash(__name__, backend="quart")) and is served through dash.testing's ThreadedRunner (the default integration-test runner, server in a worker thread), ThreadedRunner.stop() can hang at teardown — the test passes but the runner never returns.
stop() only has a graceful-shutdown branch for FastAPI, keyed on hasattr(self._app, "_uvicorn_server") (dash/testing/application_runners.py). A Quart app has no such attribute, so it falls into the else branch, which does:
The hypercorn server thread is parked in a blocking syscall (IOCP on Windows, epoll on POSIX), so the injected SystemExit isn't delivered until the loop next runs Python bytecode. The unbounded join() then blocks. Meanwhile the Quart backend's own cooperative shutdown switch — backend._ws_shutdown_event, awaited by serve(shutdown_trigger=...) — is never signalled, because only the main-thread signal handler sets it and the server is running in a worker thread.
Minimal reproduction:
import threading
from dash import Dash, Input, Output, dcc, html
from dash.testing.application_runners import ThreadedRunner
app = Dash(__name__, backend="quart")
app.layout = html.Div([dcc.Input(id="in", value="x"), html.Div(id="out")])
app.callback(Output("out", "children"), Input("in", "value"))(lambda v: v)
runner = ThreadedRunner(stop_timeout=3)
runner.start(app, host="127.0.0.1")
done = threading.Event() # flag; set only if stop() actually returns
# run stop() on a daemon thread so a hang wedges that thread, not the script
threading.Thread(target=lambda: (runner.stop(), done.set()), daemon=True).start()
# wait up to 10s (> stop_timeout); False means stop() never returned = the hang
print("stop() returned" if done.wait(10) else "HANG: stop() did not return in 10s")
Output: HANG: stop() did not return in 10s (FastAPI or Flask print stop() returned).
Expected behavior
ThreadedRunner.stop() should shut a Quart app down cleanly and return within stop_timeout, the same as it does for FastAPI/Flask.
Screenshots
If applicable, add screenshots or screen recording to help explain your problem.
Describe your context
Please provide us your environment, so we can easily reproduce the issue.
pip list | grep dashbelowDescribe the bug
When a Dash app uses the Quart backend (
Dash(__name__, backend="quart")) and is served throughdash.testing'sThreadedRunner(the default integration-test runner, server in a worker thread),ThreadedRunner.stop()can hang at teardown — the test passes but the runner never returns.stop()only has a graceful-shutdown branch for FastAPI, keyed onhasattr(self._app, "_uvicorn_server")(dash/testing/application_runners.py). A Quart app has no such attribute, so it falls into theelsebranch, which does:The hypercorn server thread is parked in a blocking syscall (IOCP on Windows, epoll on POSIX), so the injected
SystemExitisn't delivered until the loop next runs Python bytecode. The unboundedjoin()then blocks. Meanwhile the Quart backend's own cooperative shutdown switch —backend._ws_shutdown_event, awaited byserve(shutdown_trigger=...)— is never signalled, because only the main-thread signal handler sets it and the server is running in a worker thread.Minimal reproduction:
Output:
HANG: stop() did not return in 10s(FastAPI or Flask printstop() returned).Expected behavior
ThreadedRunner.stop()should shut a Quart app down cleanly and return withinstop_timeout, the same as it does for FastAPI/Flask.Screenshots
If applicable, add screenshots or screen recording to help explain your problem.