Skip to content

fix: handle with_dynamic_directory mounted at sub-path (#8322)#8434

Merged
mscolnick merged 4 commits intomainfrom
ms/dynamic-dir
Feb 26, 2026
Merged

fix: handle with_dynamic_directory mounted at sub-path (#8322)#8434
mscolnick merged 4 commits intomainfrom
ms/dynamic-dir

Conversation

@mscolnick
Copy link
Contributor

@mscolnick mscolnick commented Feb 23, 2026

fix: handle with_dynamic_directory mounted at sub-path

Fixes #8322

When mounting a marimo ASGI app at a sub-path (e.g., app.mount("/server2", server.build())), the DynamicDirectoryMiddleware failed to match requests because Starlette's Mount keeps the mount prefix in scope["path"] while also setting scope["root_path"].

For example, with with_dynamic_directory(path="/apps", ...) mounted at /server2, a request to /server2/apps/notebook/ would have path="/server2/apps/notebook/" and root_path="/server2". The middleware
checked path.startswith("/apps/") which failed because of the /server2 prefix.

Additionally, the app_builder was called with the filesystem path as base_url instead of a proper URL path.

Changes

  • Strip root_path from scope["path"] before matching base_path, so
    the middleware works regardless of where it's mounted
  • Keep fallback for frameworks that fully strip the path (not just prefix)
  • Compute proper URL-based base_url for sub-apps instead of filesystem
    paths

When mounting a marimo ASGI app at a sub-path (e.g.,
`app.mount("/server2", server.build())`), the `DynamicDirectoryMiddleware`
failed to match requests because Starlette's `Mount` keeps the mount prefix
in `scope["path"]` while also setting `scope["root_path"]`.

For example, with `with_dynamic_directory(path="/apps", ...)` mounted at
`/server2`, a request to `/server2/apps/notebook/` would have
`path="/server2/apps/notebook/"` and `root_path="/server2"`. The middleware
checked `path.startswith("/apps/")` which failed because of the `/server2`
prefix.

Additionally, the `app_builder` was called with the filesystem path as
`base_url` instead of a proper URL path.

## Changes

- Strip `root_path` from `scope["path"]` before matching `base_path`, so
  the middleware works regardless of where it's mounted
- Keep fallback for frameworks that fully strip the path (not just prefix)
- Compute proper URL-based `base_url` for sub-apps instead of filesystem
  paths
- Update `my_server.py` smoke test with two mounted servers (root +
  `/server2`)

Closes #8322
@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Feb 23, 2026 10:45pm

Request Review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes DynamicDirectoryMiddleware routing when a marimo ASGI app is mounted under a sub-path by accounting for scope["root_path"], and corrects the base_url passed into dynamically-built notebook apps so it’s a URL path (not a filesystem path).

Changes:

  • Strip root_path from scope["path"] to correctly match base_path under Starlette/FastAPI mounts.
  • Compute and pass a URL-path base_url to app_builder for dynamic directory apps.
  • Add regression tests covering sub-mount behavior (HTTP, redirects, assets, websockets) and update the custom smoke-test server example.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
marimo/_server/asgi.py Adjusts dynamic-directory request matching for mounted apps and fixes base_url computation for sub-app creation.
tests/_server/test_asgi.py Adds regression coverage for dynamic directories when the ASGI app is mounted at a sub-path.
marimo/_smoke_tests/custom_server/my_server.py Expands the manual smoke-test script to include root mount + sub-path mount scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 279 to +294
cache_key = str(marimo_file)
if cache_key not in self._app_cache:
LOGGER.debug(f"Creating new app for {cache_key}")
# Compute the URL base path for this notebook.
# This is used for template rendering (e.g., OpenGraph URLs).
try:
relative_notebook = str(
marimo_file.relative_to(self.directory)
)
if relative_notebook.endswith(".py"):
relative_notebook = relative_notebook[:-3]
# Compute the URL prefix for this notebook. When
# root_path already ends with base_path (because the
# parent mount includes it), avoid doubling the prefix.
if root_path.endswith(self.base_path):
url_prefix = root_path
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relative_notebook = str(marimo_file.relative_to(self.directory)) will use OS-specific path separators (e.g., backslashes on Windows), which can leak into notebook_base_url and produce invalid URL paths. Use a POSIX/URL-safe form (e.g., Path(...).as_posix() or replace os.sep with "/") before concatenating into a URL.

Copilot uses AI. Check for mistakes.
Comment on lines +743 to +744
the parent framework strips the prefix from scope["path"] and adds it to
scope["root_path"], causing the middleware to never match.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class docstring says the parent mount "strips the prefix from scope['path']", but the issue description for #8322 (and this test setup) is about frameworks keeping the mount prefix in scope['path'] while also setting scope['root_path']. Please update the docstring to reflect the actual behavior being tested to avoid confusion for future maintainers.

Suggested change
the parent framework strips the prefix from scope["path"] and adds it to
scope["root_path"], causing the middleware to never match.
the parent framework keeps the mount prefix in scope["path"] while also
setting scope["root_path"], causing the middleware's path matching logic
to not behave as originally expected.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +70
# Key test: dynamic directory at "/" relative to mount = "/server2/" absolute
server2 = (
marimo.create_asgi_app()
# Dynamic directory at sub-path: /apps becomes /server2/apps in FastAPI
.with_dynamic_directory(
path="/apps", directory=str(dirname / "altair_examples")
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dirname / "smoke_tests" does not exist in marimo/_smoke_tests (the smoke test notebooks live directly under dirname). As written, with_dynamic_directory(..., directory=str(dirname / "smoke_tests")) will fail at runtime. Point this at an existing directory (likely str(dirname)) or add the missing folder to the repo.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +70
# Dynamic directory at sub-path: /apps becomes /server2/apps in FastAPI
.with_dynamic_directory(
path="/apps", directory=str(dirname / "altair_examples")
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_dynamic_directory(path="/", ...) is likely not reachable: DynamicDirectoryMiddleware normalizes base_path with rstrip("/"), turning "/" into an empty string, and the request-matching logic is guarded by if self.base_path .... Use path="" for a root mount (consistent with with_app(path="", ...)) or adjust the middleware to explicitly treat an empty base_path as root.

Copilot uses AI. Check for mistakes.
@mscolnick mscolnick merged commit f3bea81 into main Feb 26, 2026
49 of 84 checks passed
@mscolnick mscolnick deleted the ms/dynamic-dir branch February 26, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

marimo asgi app mounted outside root cant locate assets

3 participants