Skip to content

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

Closed
VishakBaddur wants to merge 7 commits into
marimo-team:mainfrom
VishakBaddur:fix/virtual-file-url-resolution
Closed

fix: resolve @file virtual-file URLs to absolute in HTML renderer#9440
VishakBaddur wants to merge 7 commits into
marimo-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

@vercel

vercel Bot commented May 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 18, 2026 9:31pm

Request Review

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

Copy link
Copy Markdown
Contributor

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
@dmadisetti

Copy link
Copy Markdown
Member

Thanks! LGTM, but I think we need to make sure this is general to base URL. I'll look at it in a bit more detail but pinged @mchav who should be able to determine a bit more molab context

@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 486 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
marimo-esm 25.17MB 486 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: marimo-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/dist-*.js 23 bytes 183 bytes 14.37% ⚠️
assets/dist-*.js 299 bytes 403 bytes 287.5% ⚠️
assets/dist-*.js -128 bytes 259 bytes -33.07%
assets/dist-*.js -166 bytes 169 bytes -49.55%
assets/dist-*.js 152 bytes 256 bytes 146.15% ⚠️
assets/dist-*.js 74 bytes 176 bytes 72.55% ⚠️
assets/dist-*.js 40 bytes 177 bytes 29.2% ⚠️
assets/dist-*.js -12 bytes 164 bytes -6.82%
assets/dist-*.js 223 bytes 387 bytes 135.98% ⚠️
assets/dist-*.js -155 bytes 104 bytes -59.85%
assets/dist-*.js -116 bytes 160 bytes -42.03%
assets/dist-*.js -81 bytes 102 bytes -44.26%
assets/dist-*.js 20 bytes 276 bytes 7.81% ⚠️
assets/dist-*.js -32 bytes 137 bytes -18.93%
assets/dist-*.js -33 bytes 104 bytes -24.09%
assets/dist-*.js -8 bytes 169 bytes -4.52%
assets/dist-*.js 231 bytes 335 bytes 222.12% ⚠️
assets/dist-*.js -65 bytes 104 bytes -38.46%
assets/dist-*.js -266 bytes 137 bytes -66.0%
assets/RenderHTML-*.js 486 bytes 6.24kB 8.44% ⚠️
assets/__vite-*.js 5 bytes 98 bytes 5.38% ⚠️
assets/__vite-*.js -5 bytes 93 bytes -5.1%

Files in assets/RenderHTML-*.js:

  • ./src/plugins/core/RenderHTML.tsx → Total Size: 7.78kB

);

const title = cell.getHoverTitle?.() ?? undefined;
const isCellSelected = cell.getIsSelected?.() || false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How is this related to the current change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, that's unrelated, removed it.

// Resolve a virtual file URL (./@file/... or @file/...) to an absolute URL
// using the runtime base, ensuring a trailing slash so the notebook-ID path
// segment is never dropped during relative resolution.
function resolveVirtualFileUrl(src: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems similar to

export function asRemoteURL(path: string): URL {

Can we reuse that instead of creating a new function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've restructured it to mirror asRemoteURL's pattern (blob: handling + same base resolution). The reason I kept it as a separate function rather than calling asRemoteURL directly is that asRemoteURL doesn't guarantee a trailing slash, and without that, new URL("./@file/...", base) drops the last path segment when the base URL has no trailing slash (the exact bug we're fixing). Added a comment in the code explaining this.

});
});

describe("replaceVirtualFileSrc - virtual file URL rewriting", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These tests don't actually repro the bug. We shoudl mock the runtime manager and check against complete strings rather than substrings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the tests to mock getRuntimeManager with a base URL that has no trailing slash (http://localhost:2718/notebooks/nb_xxx), which reproduces the actual molab edit-mode scenario. Assertions now check complete URL strings instead of substrings.

@dmadisetti dmadisetti added the bug Something isn't working label May 18, 2026
VishakBaddur and others added 6 commits May 18, 2026 16:07
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
@VishakBaddur
VishakBaddur force-pushed the fix/virtual-file-url-resolution branch from 13f40f3 to 8a82c51 Compare May 18, 2026 21:30
@mscolnick

Copy link
Copy Markdown
Contributor

Going to close this so it does not get accidentally review more. the issue is on molab and it is already fixed in the latest molab that we are currently doing a slow rollout of.

@mscolnick mscolnick closed this May 18, 2026
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.

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

4 participants