Skip to content

Commit c6d0c56

Browse files
authored
fetch index.html from --asset-url as a fallback (#7453)
This is not a public facing API, but helpful when running marimo from a branch (e.g.): ``` uvx --with git+https://github.com/marimo-team/marimo.git@BRANCH \ marimo edit nb.py --asset-url="https://cdn.jsdelivr.net/npm/@marimo-team/frontend@{version}/dist" ```
1 parent cd62127 commit c6d0c56

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

marimo/_server/api/endpoints/assets.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,33 @@
7272
FILE_QUERY_PARAM_KEY = "file"
7373

7474

75+
async def _fetch_index_html_from_url(asset_url: str) -> str:
76+
"""Fetch index.html from the given asset URL."""
77+
import marimo._utils.requests as requests
78+
from marimo._version import __version__
79+
80+
# Replace {version} placeholder if present
81+
if "{version}" in asset_url:
82+
asset_url = asset_url.replace("{version}", __version__)
83+
84+
# Construct the full URL to index.html
85+
# Remove trailing slash if present
86+
asset_url = asset_url.rstrip("/")
87+
index_url = f"{asset_url}/index.html"
88+
89+
try:
90+
LOGGER.debug("Fetching index.html from: %s", index_url)
91+
response = requests.get(index_url)
92+
response.raise_for_status()
93+
return response.text()
94+
except Exception as e:
95+
LOGGER.error("Failed to fetch index.html from %s: %s", index_url, e)
96+
raise HTTPException(
97+
status_code=500,
98+
detail=f"Failed to fetch index.html from asset_url: {e}",
99+
) from e
100+
101+
75102
@router.get("/")
76103
@requires("read", redirect="auth:login_page")
77104
async def index(request: Request) -> HTMLResponse:
@@ -83,7 +110,20 @@ async def index(request: Request) -> HTMLResponse:
83110
or app_state.session_manager.file_router.get_unique_file_key()
84111
)
85112

86-
html = index_html.read_text()
113+
# Try local index.html first, fallback to asset_url if local file doesn't exist
114+
if index_html.exists():
115+
html = index_html.read_text()
116+
elif app_state.asset_url:
117+
LOGGER.info(
118+
"Local index.html not found, fetching from asset_url: %s",
119+
app_state.asset_url,
120+
)
121+
html = await _fetch_index_html_from_url(app_state.asset_url)
122+
else:
123+
raise HTTPException(
124+
status_code=500,
125+
detail="index.html not found and no asset_url configured",
126+
)
87127

88128
if not file_key:
89129
# We don't know which file to use, so we need to render a homepage

tests/_server/api/endpoints/test_assets.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
from tempfile import TemporaryDirectory
88
from typing import TYPE_CHECKING, Any, cast
9+
from unittest.mock import Mock, patch
910

1011
from marimo._server.api.deps import AppState
1112
from marimo._server.api.endpoints.assets import _inject_service_worker
@@ -217,3 +218,80 @@ def test_inject_service_worker() -> None:
217218
"const notebookId = 'c%3A%5Cpath%5Cto%5Cnotebook.py';"
218219
in _inject_service_worker("<body></body>", r"c:\path\to\notebook.py")
219220
)
221+
222+
223+
def test_index_with_missing_local_file_and_asset_url(
224+
client: TestClient,
225+
) -> None:
226+
"""Test that index.html is fetched from asset_url when local file doesn't exist."""
227+
app_state = AppState.from_app(cast(Any, client.app))
228+
229+
# Mock HTML content that would come from the CDN
230+
mock_html = """<!DOCTYPE html>
231+
<html>
232+
<head><title>{{ title }}</title></head>
233+
<body>
234+
<marimo-filename hidden>{{ filename }}</marimo-filename>
235+
'{{ mount_config }}'
236+
</body>
237+
</html>"""
238+
239+
# Mock the requests.get to return our mock HTML
240+
mock_response = Mock()
241+
mock_response.text.return_value = mock_html
242+
mock_response.raise_for_status.return_value = None
243+
244+
with (
245+
patch("marimo._server.api.endpoints.assets.root") as mock_root,
246+
patch("marimo._utils.requests.get", return_value=mock_response),
247+
):
248+
# Make local index.html appear to not exist
249+
mock_index_html = Mock()
250+
mock_index_html.exists.return_value = False
251+
mock_root.__truediv__.return_value = mock_index_html
252+
253+
# Set asset_url on the app state
254+
client.app.state.asset_url = "https://cdn.example.com/assets/0.1.0"
255+
256+
response = client.get("/", headers=token_header())
257+
assert response.status_code == 200, response.text
258+
# The response should contain processed HTML
259+
assert "<title>" in response.text
260+
261+
262+
def test_index_with_missing_local_file_no_asset_url(
263+
client: TestClient,
264+
) -> None:
265+
"""Test that error is raised when local file doesn't exist and no asset_url."""
266+
with patch("marimo._server.api.endpoints.assets.root") as mock_root:
267+
# Make local index.html appear to not exist
268+
mock_index_html = Mock()
269+
mock_index_html.exists.return_value = False
270+
mock_root.__truediv__.return_value = mock_index_html
271+
272+
# Ensure asset_url is None
273+
client.app.state.asset_url = None
274+
275+
response = client.get("/", headers=token_header())
276+
assert response.status_code == 500
277+
assert "index.html not found" in response.json()["detail"]
278+
279+
280+
def test_index_prefers_local_file_over_asset_url(client: TestClient) -> None:
281+
"""Test that local index.html is preferred even when asset_url is set."""
282+
# Set asset_url on the app state
283+
client.app.state.asset_url = "https://cdn.example.com/assets/0.1.0"
284+
285+
# Mock requests.get to track if it's called
286+
mock_response = Mock()
287+
with patch(
288+
"marimo._utils.requests.get", return_value=mock_response
289+
) as mock_get:
290+
response = client.get("/", headers=token_header())
291+
assert response.status_code == 200, response.text
292+
293+
# requests.get should NOT be called since local file exists
294+
mock_get.assert_not_called()
295+
296+
# Reset asset_url
297+
client.app.state.asset_url = None

0 commit comments

Comments
 (0)