Skip to content

Commit dcae82b

Browse files
Fix resource paths under Windows extended-length paths
Windows uses a path with the \\?\ prefix verbatim, so a "/" inside it is an invalid name (WinError 123). Dash joined "/"-separated package paths, and a nested favicon's asset path, onto a directory and passed the result to os.stat, so every index render failed with a 500 when Dash was imported from such a path, as in JupyterLab Desktop's site-packages. Split those paths on "/" before joining them in _relative_url_path, _get_worker_url and the favicon mtime lookup. Fixes #3002
1 parent 9c56933 commit dcae82b

3 files changed

Lines changed: 35 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
3737
- [#3881](https://github.com/plotly/dash/pull/3881) Fix components rendered as props (eg. `dcc.Dropdown` option labels, `dcc.Tab` labels) crashing or failing to update when the host subtree was replaced by a callback; out-of-tree `ExternalWrapper` components now re-insert themselves and update in place.
3838
- [#3929](https://github.com/plotly/dash/issues/3929) Fix components that set their own initial state on mount (eg. `dash-bootstrap-components` `Tabs`) not applying it on first render, because descendant layout hashes were reset on the first fresh render (regression introduced in 4.2.0 by [#3570](https://github.com/plotly/dash/pull/3570)).
3939
- [#3948](https://github.com/plotly/dash/issues/3948) Fix page getting progressively slower as callbacks append children
40+
- [#3002](https://github.com/plotly/dash/issues/3002) Fix page loads failing with a 500 error on Windows when Dash is imported from an extended-length (`\\?\`) path, as in JupyterLab Desktop. Package resource paths, and the path of a `favicon.ico` in an assets subfolder, are now joined with the OS separator instead of `/`.
4041

4142
## [4.4.1] - 2026-07-21
4243

‎dash/dash.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,7 +1261,7 @@ def _get_worker_url(
12611261
# Build fingerprinted URL (same pattern as _collect_and_register_resources)
12621262
module_path = os.path.join(
12631263
os.path.dirname(sys.modules[namespace].__file__), # type: ignore
1264-
relative_path,
1264+
*relative_path.split("/"),
12651265
)
12661266

12671267
# Use a fallback if the file doesn't exist yet (during development)
@@ -1302,9 +1302,11 @@ def _relative_url_path(relative_package_path="", namespace=""):
13021302
else:
13031303
version = importlib.import_module(namespace).__version__
13041304

1305+
# Split on "/" so the file path uses the OS separator: Windows
1306+
# extended-length paths (\\?\ prefix) reject forward slashes.
13051307
module_path = os.path.join( # type: ignore[reportCallIssue]
13061308
os.path.dirname(sys.modules[namespace].__file__), # type: ignore[reportCallIssue]
1307-
relative_package_path,
1309+
*relative_package_path.split("/"),
13081310
)
13091311

13101312
modified = int(os.stat(module_path).st_mtime)
@@ -1489,7 +1491,7 @@ def index(self, *_args, **_kwargs):
14891491

14901492
if self._favicon:
14911493
favicon_mod_time = os.path.getmtime(
1492-
os.path.join(self.config.assets_folder, self._favicon)
1494+
os.path.join(self.config.assets_folder, *self._favicon.split("/"))
14931495
)
14941496
favicon_url = f"{self.get_asset_url(self._favicon)}?m={favicon_mod_time}"
14951497
else:

‎tests/unit/test_resources.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import os
2+
import sys
3+
14
import mock
5+
import pytest
26
import dash
37
from dash import dcc, html # noqa: F401
48

@@ -253,3 +257,28 @@ def test_multiple_external_urls_with_attributes():
253257
{"src": "https://example.com/script1.js", "type": "module"},
254258
{"src": "https://example.com/script2.js", "type": "module"},
255259
]
260+
261+
262+
@pytest.mark.skipif(
263+
sys.platform != "win32", reason="extended-length paths only exist on Windows"
264+
)
265+
def test_index_with_windows_extended_length_paths(tmp_path, monkeypatch):
266+
"""Dash and the assets folder under extended-length paths (JupyterLab Desktop).
267+
268+
Windows uses these paths verbatim, so a "/" inside one is an invalid name.
269+
"""
270+
monkeypatch.setattr(dash, "__file__", "\\\\?\\" + os.path.abspath(dash.__file__))
271+
icons = tmp_path / "assets" / "icons"
272+
icons.mkdir(parents=True)
273+
(icons / "favicon.ico").write_bytes(b"")
274+
275+
app = dash.Dash(__name__, assets_folder="\\\\?\\" + str(tmp_path / "assets"))
276+
app.layout = html.Div()
277+
278+
response = app.server.test_client().get("/")
279+
280+
assert response.status_code == 200
281+
body = response.get_data(as_text=True)
282+
assert "/_dash-component-suites/dash/deps/polyfill@" in body
283+
assert "dash-stream-worker.v" in body
284+
assert "/assets/icons/favicon.ico?m=" in body

0 commit comments

Comments
 (0)