fix(media): serve and render .md files with inline Markdown preview#6277
fix(media): serve and render .md files with inline Markdown preview#6277webtecnica wants to merge 3 commits into
Conversation
Closes common issue where clicking MEDIA: links for .md files could download a JSON auth error on desktop. Three-layer fix: 1. MIME mapping (api/config.py): add .md/.mkd/.mkdn → text/markdown so the server returns the correct Content-Type instead of application/octet-stream 2. Session allow-list (api/routes.py): add text/markdown to _SESSION_MEDIA_TOKEN_TYPES so .md files are accepted by the session-token path check 3. Frontend preview (static/ui.js + static/style.css): detect .md files in _inlineMediaHtmlForRef, fetch content via api/media, render through the existing renderMd() pipeline, and display inline in a styled container — same pattern as HTML/CSV previews
|
| Filename | Overview |
|---|---|
| api/config.py | Adds .md, .mkd, .mkdn → text/markdown MIME entries; straightforward and correct. |
| api/routes.py | Adds text/markdown to _SESSION_MEDIA_TOKEN_TYPES; _INLINE_PREVIEW_TYPES correctly left unchanged since the JS preview uses fetch/text() rather than the browser's inline rendering path. |
| static/style.css | Adds CSS for the markdown inline preview; .md-inline-content has no max-height, so very long files under the 256 KB cap render without a scrollable bound. |
| static/ui.js | Adds _MD_EXTS, loadMarkdownInline(), and hooks into postProcessRenderedMessages; correctly mirrors the CSV/HTML loader pattern, but fallback/error downloadUrl embeds the session_id and i18n keys are missing (both flagged in prior review threads). |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Browser
participant UI as ui.js
participant Server as api/routes.py
Note over Browser,Server: postProcessRenderedMessages() fires after render
UI->>UI: loadMarkdownInline(container)
UI->>UI: find .md-inline-load[data-path] elements
UI->>Server: "fetch(api/media?path=...&session_id=...)"
Server->>Server: _session_media_token_allows_path()
Server->>Server: MIME_MAP[".md"] returns text/markdown
Server-->>UI: 200 OK, Content-Type: text/markdown
alt "text.length <= 256 KB"
UI->>UI: renderMd(text)
UI->>Browser: outerHTML set to md-inline-wrap
else "text.length > 256 KB"
UI->>Browser: outerHTML set to md-inline-fallback
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Browser
participant UI as ui.js
participant Server as api/routes.py
Note over Browser,Server: postProcessRenderedMessages() fires after render
UI->>UI: loadMarkdownInline(container)
UI->>UI: find .md-inline-load[data-path] elements
UI->>Server: "fetch(api/media?path=...&session_id=...)"
Server->>Server: _session_media_token_allows_path()
Server->>Server: MIME_MAP[".md"] returns text/markdown
Server-->>UI: 200 OK, Content-Type: text/markdown
alt "text.length <= 256 KB"
UI->>UI: renderMd(text)
UI->>Browser: outerHTML set to md-inline-wrap
else "text.length > 256 KB"
UI->>Browser: outerHTML set to md-inline-fallback
end
Reviews (3): Last reviewed commit: "fix: keep session_id in download URL for..." | Re-trigger Greptile
| const MD_MAX_SIZE = 256 * 1024; | ||
|
|
||
| function loadMarkdownInline(container){ |
There was a problem hiding this comment.
MD_MAX_SIZE is declared at module scope while HTML_MAX_SIZE in loadHtmlInline (and PDF_MAX_SIZE in loadPdfInline) are declared inside their respective functions. Keeping the constant local to loadMarkdownInline makes the coupling obvious and matches the convention used by every other inline loader.
| const MD_MAX_SIZE = 256 * 1024; | |
| function loadMarkdownInline(container){ | |
| function loadMarkdownInline(container){ | |
| const MD_MAX_SIZE = 256 * 1024; // 256 KB cap for inline Markdown preview |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…nesquena#6277) - downloadUrl now built from publicMediaUrl instead of mediaUrl (matching HTML/PDF/excalidraw loaders convention) - MD_MAX_SIZE at module scope kept — consistent with CSV_MAX_SIZE, HTML_MAX_SIZE, EXCALIDRAW_MAX_SIZE, PDF_MAX_SIZE
|
✅ Ajusted: download link usa |
| if(_MD_EXTS.test(ref)){ | ||
| return `<div class="md-inline-load" data-path="${esc(ref)}"><span class="md-preview-spinner">⏳</span> ${esc(typeof t==='function'?t('md_loading'):'Loading')}...</div>`; |
There was a problem hiding this comment.
Missing i18n keys produce raw key names in the UI
t('md_loading'), t('md_too_large'), and t('md_error') are called throughout _inlineMediaHtmlForRef and loadMarkdownInline, but none of these keys are registered in i18n.js. The t() function's final fallback (line 26001 of i18n.js: if (val === undefined) return key;) means every user will literally see "md_loading" in the loading spinner, "md_too_large" in the size-cap path, and "md_error" in the catch block — permanently, not just transiently. Compare html_loading, html_too_large, and html_error, which are defined across all supported locales. The hardcoded English strings in the ternary ('File too large for preview', 'Loading', 'Error loading markdown') are dead code because t is always a function in production.
🎬 Cutter preview — PR #6277
|
SummaryThis one-line change regresses the authorization path for Markdown artifacts outside the globally allowed media roots. The preview request keeps Code references
The URL construction should remain session-aware: const publicMediaUrl = 'api/media?path=' + encodeURIComponent(path);
const mediaUrl = publicMediaUrl + (mediaSessionId ? '&session_id=' + encodeURIComponent(mediaSessionId) : '');
const downloadUrl = mediaUrl + '&download=1';Why this mattersThe session identifier here is not cosmetic state. It is the capability that lets Suggested verificationAdd a focused browser or JavaScript test for a Markdown |


Problem
MEDIA: links for Markdown (
.md) files intermittently download a JSON{"error":"Authentication required"}file on desktop, instead of the actual file.Root cause:
.mdwas missing fromMIME_MAP→ fell back toapplication/octet-stream, and the frontend only showed a bare<a download>link (no inline preview). When the auth cookie wasn't included in the download request, the 401 JSON response was saved as the downloaded file.Changes
1. MIME mapping (
api/config.py)Add
.md,.mkd,.mkdn→text/markdownso the server returns the correctContent-Type.2. Session allow-list (
api/routes.py)Add
"text/markdown"to_SESSION_MEDIA_TOKEN_TYPESso.mdfiles pass the session-token media check (alongside the existing root-allowlist check).3. Frontend Markdown inline preview (
static/ui.js+static/style.css)_MD_EXTSregex constant for.md/.mkd/.mkdn_inlineMediaHtmlForRefrenders.mdfiles as a loading placeholder (<div class="md-inline-load">) instead of a bare<a download>linkloadMarkdownInline()fetches the file content viaapi/media, renders it through the existingrenderMd()pipeline, and inserts it inline — same pattern as CSV/HTML/excalidraw previewspostProcessRenderedMessages()Behavior after fix
.mddownload link → auth failure downloads 401 JSONrenderMd()pipeline as chat messagesContent-Type: application/octet-streamContent-Type: text/markdown