Skip to content

Conversation

@BSd3v
Copy link
Contributor

@BSd3v BSd3v commented Sep 9, 2025

This is an open PR draft, to contribute please target my forked branch.

The goal of this PR is to modularize the Dash setup to be independent of Flask (will fallback to Flask) and allow devs to configure their own backend.

  • decouple callback context from Flask Request
  • decouple index, assets, etc from build
  • build Quart server support
  • build FastAPI server support
  • allow for custom server build
  • add tests for servers

fixes #1571

@gvwilson gvwilson added feature something new P2 considered for next cycle community community contribution labels Sep 10, 2025
BSd3v and others added 18 commits September 16, 2025 16:18
…e flow. Made endpoints for downloading the reqs
* ∙ - remove contextvar from flask and quart only FastApi now relies on that
∙ - backend __init__ now holds the global request adapter and backend which get set on app initialisation
∙ request adapter and server can now be call from everywhere after the app initialised
∙ - added normal top level imports because the modules get matching loaded - but bad Import Error message when quart or equivilent are not installed
∙ - added _ as prefix to backends to avoid importing errors with their underlying
∙ - Can now move to remove unnecessary passing of the server object
∙

* Moved get_server_type to backends

* ∙ moved async validation to validation
∙ replaced request.get_path with request.path
∙

* Moved custom backend check to _validation.py

* Removed server injection of server methods - they use self.server now

* removed use_async from dispatch server methods and use dash_app._use_async
removed remaining set request process from flask

* adding custom error handling per backend, tests and adjustments to the flow. Made endpoints for downloading the reqs

* adjusments for formatting

* adjustment to retest backend

* Added Dash app as type to servers

* adding missing reqs association

* Addedd basic typing to servers

* fixing minor linting issues

* Fixed weird AI shit

* Cleanup before heavy pull

* Merged latest changes

* f rebase

* f rebase

* Added Dash app as type to servers

* Addedd basic typing to servers

---------

Co-authored-by: Christian Giessel <cgiessel@tesla.com>
Co-authored-by: BSd3v <82055130+BSd3v@users.noreply.github.com>
@BSd3v BSd3v marked this pull request as ready for review September 18, 2025 20:34
BSd3v and others added 6 commits September 22, 2025 08:30
* ∙ Added has_request_context to base_server
∙ removed flask specific import in _validate and use backends.backend.has_request_context now

* ∙ added context to request adapter
∙ callback context uses request adapter context now
∙

* added get_root_path to dash _utils
removed flask.helpers.get_root_path usage

* moved compress to server implementations
flask fully decoupled from dash

* fixed compress in quart and fastapi

* Fixed server.config in fastapi to use config file

* Update dash/backends/_fastapi.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* removed unused flask import in pages

* Update dash/backends/_quart.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* removed flask specific return type to remove global dependency

---------

Co-authored-by: Christian Giessel <cgiessel@tesla.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

@T4rk1n T4rk1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good, a few fixes are needed and I left a few suggestion.

  • The serve_health route in dash.py still using flask.Response directly and need to be added in the backends.
  • There is a few Flask references left in comments on the Dash class that need to be changed.
  • The fastapi app is not running in debug mode if the app filename name is not "app.py".

"""

def wrap(func: _t.Callable[[], _f.Response]):
def wrap(func: _t.Callable[[], _t.Any]):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a future pain point to update existing hooks, maybe we can try adding a flask.Response converter in the adapter?

Comment on lines +8 to +12
_backend_imports = {
"flask": ("dash.backends._flask", "FlaskDashServer"),
"fastapi": ("dash.backends._fastapi", "FastAPIDashServer"),
"quart": ("dash.backends._quart", "QuartDashServer"),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have a way for this to be customizable, maybe add a hook for that. Then the backend library can have the hook inside it's init.

<textarea readonly>{formatted_tb}</textarea>
</div>
<div class=\"explanation\">
The debugger caught an exception in your ASGI application. You can now
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The debugger caught an exception in your ASGI application. You can now
The debugger caught an exception in your Dash application. You can now

Comment on lines +34 to +42
@property # pragma: no cover - interface
@abstractmethod
def cookies(self):
raise NotImplementedError()

@property # pragma: no cover - interface
@abstractmethod
def headers(self):
raise NotImplementedError()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can type those as dict, then make sure the backends returns a dict copied from all the cookies/headers.

In the same way, maybe we can add a single get_cookie(...) and get_header(...) for single usage.

Comment on lines +121 to +125
@abstractmethod
def make_response(
self, data, mimetype=None, content_type=None
) -> Any: # pragma: no cover - interface
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usual usecase for the response in the dash context is to set cookies/headers, maybe we should a global set_cookie(...) and set_header akin to set_props that can be used across the different backends with ease.

if kwargs.get("reload"):
# Dynamically determine the module name from the file path
file_path = frame.filename
spec = spec_from_file_location("app", file_path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the real file here, when I try with an app not named app with debug=True I get:

INFO:     Uvicorn running on http://127.0.0.1:8050 (Press CTRL+C to quit)
INFO:     Started reloader process [80425] using StatReload
ERROR:    Error loading ASGI app. Could not import module "app".
WARNING:  StatReload detected changes in 'quart_app.py'. Reloading...
ERROR:    Error loading ASGI app. Could not import module "app".

include_in_schema=False,
)

def dispatch(self, dash_app: Dash):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good time to rename this to a better name like serve_callback.

Comment on lines +325 to +326
if Response is None:
raise RuntimeError("Quart not installed; cannot generate Response")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be happening, we should raise back the import error with a custom message to install dash[quart].

Comment on lines +327 to +329
return Response(
pkgutil.get_data("dash", "favicon.ico"), content_type="image/x-icon"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same for all the backends, maybe it be in the base and just call the proper make_response to handle that?

Comment on lines +211 to +233
def serve_component_suites(
self, dash_app: Dash, package_name: str, fingerprinted_path: str
): # noqa: ARG002 unused req preserved for interface parity
path_in_pkg, has_fingerprint = check_fingerprint(fingerprinted_path)
_validate.validate_js_path(dash_app.registered_paths, package_name, path_in_pkg)
extension = "." + path_in_pkg.split(".")[-1]
mimetype = mimetypes.types_map.get(extension, "application/octet-stream")
package = sys.modules[package_name]
dash_app.logger.debug(
"serving -- package: %s[%s] resource: %s => location: %s",
package_name,
getattr(package, "__version__", "unknown"),
path_in_pkg,
package.__path__,
)
data = pkgutil.get_data(package_name, path_in_pkg)
headers = {}
if has_fingerprint:
headers["Cache-Control"] = "public, max-age=31536000"

if Response is None:
raise RuntimeError("Quart not installed; cannot generate Response")
return Response(data, content_type=mimetype, headers=headers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function will mostly be the same across backends with a difference in the Response type, in this case we are missing the etag stuff that might need to be adapted.

I think maybe we could have a Response type that is dash specific and then we have a ResponseAdapter like we do for the RequestAdapter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community community contribution feature something new P2 considered for next cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

provide support for FastAPI / other ASGI

4 participants