You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] dash.testing ThreadedRunner.stop() hangs at teardown for Quart backend apps #3823
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:
importthreadingfromdashimportDash, Input, Output, dcc, htmlfromdash.testing.application_runnersimportThreadedRunnerapp=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"))(lambdav: 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 scriptthreading.Thread(target=lambda: (runner.stop(), done.set()), daemon=True).start()
# wait up to 10s (> stop_timeout); False means stop() never returned = the hangprint("stop() returned"ifdone.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.
Activity