Skip to content

Commit d74f615

Browse files
authored
Merge pull request #4074 from b84330808/fix-pages-pathname-decoding
Decode the pages router pathname before matching it
2 parents 40c6f7f + 92d6121 commit d74f615

3 files changed

Lines changed: 55 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
5959
- [#4023](https://github.com/plotly/dash/pull/4023) Fix extra callback registrations when an app runs from the parent directory: `python app/app.py`, where the script shares its folder's name, registered its callbacks twice, and `python app/main.py` imported a sibling `app/app.py` as the parent package and registered its callbacks too (regression introduced in 4.4.1 by [#3883](https://github.com/plotly/dash/pull/3883)). Fixes [#4011](https://github.com/plotly/dash/issues/4011).
6060
- [#4057](https://github.com/plotly/dash/pull/4057) Fixed background callbacks to update `set_props` instead of replacing. Fixes [#3104](https://github.com/plotly/dash/issues/3104).
6161
- [#3923](https://github.com/plotly/dash/pull/3923) Fix a blank page and `SyntaxError` in the browser when a dict component ID has a dot in one of its values. Fixes [#3480](https://github.com/plotly/dash/issues/3480).
62+
- [#4074](https://github.com/plotly/dash/pull/4074) Fix Pages passing percent-encoded path variables to `layout` and `title`, and the 404 page for paths with spaces or non-ASCII characters. Fixes [#2360](https://github.com/plotly/dash/issues/2360).
6263
- [#4012](https://github.com/plotly/dash/pull/4012) Fix `InvalidCallbackReturnValue` raised when a no-output background callback's error handler returns `None` (or the job is cancelled), by treating `None` returns from error handlers and the cancelled-job branch the same way — as `NoUpdate()` only when there are outputs, `None` otherwise. Fixes [#3628](https://github.com/plotly/dash/issues/3628).
6364

6465
## [4.4.1] - 2026-07-21

‎dash/dash.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import mimetypes
1818
import hashlib
1919
import base64
20-
from urllib.parse import urlparse
20+
from urllib.parse import unquote, urlparse
2121
from typing import (
2222
Any,
2323
Callable,
@@ -2851,7 +2851,9 @@ async def router_async():
28512851
async def update(pathname_, search_, **states):
28522852
query_parameters = _parse_query_string(search_)
28532853
page, path_variables = _path_to_page(
2854-
self.strip_relative_path(pathname_)
2854+
self.strip_relative_path(
2855+
unquote(pathname_) if pathname_ else pathname_
2856+
)
28552857
)
28562858
if page == {}:
28572859
for module, page in _pages.PAGE_REGISTRY.items():
@@ -2939,7 +2941,9 @@ def router_sync():
29392941
def update(pathname_, search_, **states):
29402942
query_parameters = _parse_query_string(search_)
29412943
page, path_variables = _path_to_page(
2942-
self.strip_relative_path(pathname_)
2944+
self.strip_relative_path(
2945+
unquote(pathname_) if pathname_ else pathname_
2946+
)
29432947
)
29442948
if page == {}:
29452949
for module, page in _pages.PAGE_REGISTRY.items():

‎tests/unit/pages/test_pages.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44
import dash
5-
from dash import Dash, _pages
5+
from dash import Dash, _pages, html
66
from mock import patch
77

88

@@ -76,3 +76,49 @@ def test_import_layouts_from_pages(
7676

7777
page_entry = list(dash.page_registry.values())[0]
7878
assert page_entry["module"] == expected_module_name
79+
80+
81+
def _route(client, pathname):
82+
response = client.post(
83+
"/_dash-update-component",
84+
json={
85+
"output": ".._pages_content.children..._pages_store.data..",
86+
"outputs": [
87+
{"id": "_pages_content", "property": "children"},
88+
{"id": "_pages_store", "property": "data"},
89+
],
90+
"inputs": [
91+
{"id": "_pages_location", "property": "pathname", "value": pathname},
92+
{"id": "_pages_location", "property": "search", "value": ""},
93+
],
94+
"state": [],
95+
"changedPropIds": ["_pages_location.pathname"],
96+
},
97+
)
98+
return response.get_json()["response"]["_pages_content"]["children"]
99+
100+
101+
@pytest.mark.parametrize("use_async", [False, True])
102+
def test_router_decodes_pathname(clear_pages_state, use_async):
103+
if use_async:
104+
pytest.importorskip("asgiref")
105+
app = Dash(__name__, use_pages=True, pages_folder="", use_async=use_async)
106+
dash.register_page(
107+
"report",
108+
path_template="/report/<name>",
109+
layout=lambda name=None, **_: html.Div(name, id="name"),
110+
)
111+
dash.register_page("cafe", path="/caf\u00e9", layout=html.Div(id="cafe"))
112+
app.layout = html.Div(dash.page_container)
113+
client = app.server.test_client()
114+
client.get("/")
115+
116+
# dcc.Location sends window.location.pathname, which browsers percent-encode
117+
assert _route(client, "/report/hello%20world")["props"]["children"] == (
118+
"hello world"
119+
)
120+
assert _route(client, "/report/%E6%97%A5%E6%9C%AC")["props"]["children"] == (
121+
"\u65e5\u672c"
122+
)
123+
assert _route(client, "/report/a%0Ab")["props"]["children"] == "a\nb"
124+
assert _route(client, "/caf%C3%A9")["props"]["id"] == "cafe"

0 commit comments

Comments
 (0)