Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed
- [#3827](https://github.com/plotly/dash/issues/3827) Fix Flask-WTF `CSRFProtect` (and Quart-WTF) exemptions breaking after the backend refactor. The callback dispatch view is now exposed with the fully-qualified name `dash.dash.dispatch` again, so `csrf._exempt_views.add("dash.dash.dispatch")` works as it did in Dash 4.1.0.

## [4.4.0] - 2026-07-03

### Added
Expand Down
10 changes: 10 additions & 0 deletions dash/backends/_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,16 @@ async def _dispatch_async():
response_data = await response_data
return cb_ctx.dash_response.set_response(data=response_data)

# Preserve the view function's identity as `dash.dash.dispatch` so that
# integrations keying on the fully-qualified view name keep working after
# the backend refactor moved dispatching out of `dash.dash.Dash.dispatch`.
# e.g. Flask-WTF CSRFProtect exemptions:
# csrf._exempt_views.add("dash.dash.dispatch")
for _fn in (_dispatch, _dispatch_async):
_fn.__name__ = "dispatch"
_fn.__qualname__ = "dispatch"
_fn.__module__ = "dash.dash"

if dash_app._use_async: # pylint: disable=protected-access
return _dispatch_async
return _dispatch
Expand Down
9 changes: 9 additions & 0 deletions dash/backends/_quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,15 @@ async def _dispatch():
response_data = await response_data
return cb_ctx.dash_response.set_response(data=response_data) # type: ignore[arg-type]

# Preserve the view function's identity as `dash.dash.dispatch` so that
# integrations keying on the fully-qualified view name keep working after
# the backend refactor moved dispatching out of `dash.dash.Dash.dispatch`.
# e.g. (Quart-)WTF CSRFProtect exemptions:
# csrf._exempt_views.add("dash.dash.dispatch")
_dispatch.__name__ = "dispatch"
_dispatch.__qualname__ = "dispatch"
_dispatch.__module__ = "dash.dash"

return _dispatch

def register_callback_api_routes(
Expand Down
31 changes: 31 additions & 0 deletions tests/backend_tests/test_dispatch_view_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Regression tests for the callback dispatch view function identity.

Integrations such as Flask-WTF's ``CSRFProtect`` exempt views by their
fully-qualified name (``f"{view.__module__}.{view.__name__}"``). Prior to the
backend refactor the dispatch view was ``dash.dash.Dash.dispatch``, so users
exempt it with ``csrf._exempt_views.add("dash.dash.dispatch")``. The refactor
must keep exposing that same name so those exemptions keep working.

See https://github.com/plotly/dash/issues/3827
"""
import pytest
from dash import Dash, html


def _dispatch_view_dest(app):
view = app.server.view_functions["/_dash-update-component"]
return f"{view.__module__}.{view.__name__}"


def test_flask_dispatch_view_name():
app = Dash(__name__)
app.layout = html.Div()
assert _dispatch_view_dest(app) == "dash.dash.dispatch"


def test_quart_dispatch_view_name():
pytest.importorskip("quart")
app = Dash(__name__, backend="quart")
app.layout = html.Div()
view = app.server.view_functions["/_dash-update-component"]
assert f"{view.__module__}.{view.__name__}" == "dash.dash.dispatch"
Loading