Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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.
Comment thread
T4rk1n marked this conversation as resolved.
Outdated

## [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