Skip to content

fix: resolve @file virtual-file URLs to absolute in HTML renderer#9440

Open
VishakBaddur wants to merge 4 commits intomarimo-team:mainfrom
VishakBaddur:fix/virtual-file-url-resolution
Open

fix: resolve @file virtual-file URLs to absolute in HTML renderer#9440
VishakBaddur wants to merge 4 commits intomarimo-team:mainfrom
VishakBaddur:fix/virtual-file-url-resolution

Conversation

@VishakBaddur
Copy link
Copy Markdown
Contributor

Problem

mo.image() (and mo.audio(), mo.pdf()) fails to render in edit mode on molab but works in app mode. Closes #9432.

Root cause: The backend intentionally generates relative virtual file URLs like ./@file/SIZE-filename (it can't know the mount path at creation time). When the page URL has no trailing slash — e.g., molab edit mode at /notebooks/nb_xxx — the browser resolves ./@file/... to /notebooks/@file/..., dropping the notebook ID and causing a 404.

App mode works because the URL ends with /app, providing an extra path segment that makes relative resolution correct.

Verified empirically:

new URL('./@file/123.png', 'https://molab.marimo.io/notebooks/nb_xxx')
// → https://molab.marimo.io/notebooks/@file/123.png  ❌ (notebook ID dropped)

new URL('./@file/123.png', 'https://molab.marimo.io/notebooks/nb_xxx/app')
// → https://molab.marimo.io/notebooks/nb_xxx/@file/123.png  ✓

Fix

Add resolveVirtualFileUrl() helper in RenderHTML.tsx that resolves @file/ URLs against the runtime base URL with a guaranteed trailing slash, making the URL absolute and unambiguous regardless of the page URL's trailing slash.

Apply it in:

  • replaceVirtualFileSrc — handles <img>, <audio>, <video>, <source> (covers mo.image(), mo.audio())
  • replaceValidIframes — handles <iframe> (covers mo.pdf())

Data URLs and external https:// URLs are untouched.

Testing

  • 4 new regression tests: ./@file/ rewriting, @file/ without ./, non-@file URLs (unchanged), data: URLs (unchanged)
  • 305/305 test files pass, 4722/4722 tests pass, zero regressions

When mo.image() (or mo.audio(), mo.pdf()) renders output, the backend
generates a relative URL like ./@file/SIZE-filename. On molab in edit
mode, the page URL is /notebooks/nb_xxx (no trailing slash), so the
browser resolves ./@file/... to /notebooks/@file/... — dropping the
notebook ID — causing a 404 and broken image.

App mode works because the URL ends with /app, giving an extra path
segment for relative resolution.

Fix: add resolveVirtualFileUrl() helper that gets the runtime base URL
and guarantees a trailing slash before resolving the @file path to
absolute. Apply it in replaceVirtualFileSrc (img/audio/video/source)
and replaceValidIframes (iframe, for mo.pdf()).

Fixes marimo-team#9432
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 3, 2026 2:30am

Request Review

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/plugins/core/RenderHTML.tsx">

<violation number="1" location="frontend/src/plugins/core/RenderHTML.tsx:64">
P2: `replaceVirtualFileSrc` recreates `<audio>/<video>` without children, which drops nested `<source>/<track>` and fallback content.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant BE as Marimo Backend
    participant RHTML as RenderHTML Component
    participant RM as RuntimeManager
    participant Browser as Browser DOM

    Note over BE, Browser: User loads notebook (e.g., /notebooks/nb_123)

    BE->>RHTML: Send HTML (mo.image, mo.pdf, etc.)
    Note right of BE: contains src="./@file/..."

    RHTML->>RHTML: parseHtml()
    
    loop For each img, audio, video, source, iframe
        RHTML->>RHTML: NEW: Check if src contains "/@file/"
        
        opt Match Found
            RHTML->>RM: getRuntimeManager().httpURL
            RM-->>RHTML: Return base URL
            
            RHTML->>RHTML: NEW: resolveVirtualFileUrl(src)
            Note right of RHTML: 1. Ensure base URL has trailing slash "/"<br/>2. Resolve relative src against base<br/>3. Convert to Absolute URL
        end
    end

    alt CHANGED: Tag is media (img/audio/video/source)
        RHTML->>RHTML: NEW: replaceVirtualFileSrc()
        RHTML-->>Browser: Render element with absolute src
    else CHANGED: Tag is iframe
        RHTML->>RHTML: CHANGED: replaceValidIframes() (absolute src injection)
        RHTML-->>Browser: Render iframe with absolute src
    else Default (External URL or Data URI)
        RHTML-->>Browser: Render element with original src
    end

    Note over Browser, BE: Network Request Resolution
    
    alt Original Behavior (Buggy in Edit Mode)
        Browser-xBE: GET /notebooks/@file/... (404: nb_123 segment lost)
    else NEW: Fixed Behavior
        Browser->>BE: GET /notebooks/nb_123/@file/...
        BE-->>Browser: 200 OK (Asset data)
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread frontend/src/plugins/core/RenderHTML.tsx Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mo.image() not rendered on molab.marimo.io in edit mode

1 participant