Skip to content

fix(chainlit): render source PDF previews in the chat side panel - #606

Merged
Ahmath-Gadji merged 5 commits into
refactor/hexagonalfrom
fix/chainlit-pdf-preview
Jul 1, 2026
Merged

fix(chainlit): render source PDF previews in the chat side panel#606
Ahmath-Gadji merged 5 commits into
refactor/hexagonalfrom
fix/chainlit-pdf-preview

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Source PDF previews in the Chainlit chat side panel were broken — clicking a source rendered "Failed to load PDF file" even though the document was indexed and retrievable. This turned out to be two independent bugs; fixing only one is not enough.

Symptom Root cause
The PDF panel shows "Failed to load PDF file" and the PDF is never even fetched react-pdf's pdf.js worker can't load — it's requested at the origin-root /assets/… which isn't auth-bypassed → 403 application/json → the browser blocks the module worker on a forbidden MIME type
Even with the worker loading, the byte stream would download instead of display /static/{chunk_id} served the file as Content-Disposition: attachment (a download), not inline

The browser console made the primary cause explicit:

Loading the worker at http://<host>/assets/pdf.worker.min-*.mjs was blocked due to a forbidden MIME type ("application/json").


Root cause

Chainlit is sub-mounted at /chainlit, and its HTML asset references are rewritten to /chainlit/assets/*. But react-pdf computes its worker URL at runtime in JS as the origin-root /assets/pdf.worker*.mjs — the HTML root-path rewrite never sees it, so it misses the /chainlit prefix and lands on a path that isn't auth-bypassed.

Before — the worker request is rejected

sequenceDiagram
    participant B as Browser<br/>(Chainlit UI @ /chainlit)
    participant A as OpenRag API

    Note over B: react-pdf boots its pdf.js worker
    B->>A: GET /assets/pdf.worker.min-*.mjs
    Note over A: /assets is NOT under the<br/>/chainlit auth bypass,<br/>and nothing serves it
    A-->>B: 403 {"detail":"Missing token"}<br/>Content-Type: application/json
    Note over B: module worker blocked —<br/>forbidden MIME "application/json"
    Note over B: ❌ "Failed to load PDF file"<br/>(the PDF /static request never happens)
Loading

After — the worker (and the PDF) load

sequenceDiagram
    participant B as Browser<br/>(Chainlit UI @ /chainlit)
    participant A as OpenRag API

    B->>A: GET /assets/pdf.worker.min-*.mjs
    Note over A: /assets/* bypassed in auth +<br/>StaticFiles mount serves the file
    A-->>B: 200 Content-Type: application/javascript
    Note over B: pdf.js worker starts ✅
    B->>A: GET /static/{chunk_id}?token=… (Range)
    Note over A: FileResponse, Content-Disposition: inline
    A-->>B: 206 Partial Content · application/pdf
    Note over B: ✅ PDF renders in the side panel
Loading

Changes

openrag/api/main.py — serve Chainlit's frontend asset dir at the origin-root /assets too (in addition to /chainlit/assets), so the runtime-computed worker URL resolves with a JavaScript MIME type.

openrag/api/middleware/auth.py — bypass /assets/* in AuthMiddleware. These are public frontend bundles — the same files already served (and already bypassed) under /chainlit/assets — so the module worker gets application/javascript instead of a 403 application/json.

openrag/api/routers/user/download.py — serve /static/{chunk_id} inline for safe previewable media types (PDF / image / audio / video) so the viewers render in place. FileResponse defaults to attachment when filename is set. HTML / SVG deliberately stay attachment so a crafted indexed file can't turn this same-origin route into a stored-XSS vector.

Note: the two /assets changes are a pair — the main.py mount serves the file (turns 404200), the auth.py bypass lets the request reach it (turns 403→ reaches the mount). Removing either regresses the preview.


Verification

Fresh request (no browser cache), against a running stack:

Request Before After
GET /assets/pdf.worker.min-*.mjs 403 application/json (worker blocked) 200 application/javascript
GET /static/{chunk_id}?token=… 200 … attachment (download) inline · application/pdf (renders); range fetches return 206
Path traversal on the new mount (/assets/../…) blocked (403 / 404)

Confirmed end-to-end in a private/incognito tab (no cache): the source PDF now renders in the Chainlit side panel.

  • ruff check clean on all three files.
  • pytest tests/unit/api/routers/user/test_download.py — passing.

Summary by CodeRabbit

  • New Features

    • Improved Chainlit UI asset delivery by serving required frontend/static files from the root /assets path (including the pdf.js worker).
  • Bug Fixes

    • Prevented public asset requests from being blocked by expanding the authentication bypass to include /assets/.
    • Fixed standalone source downloads by restoring the correct request-token context.
  • Security / Improvements

    • Updated static file downloads to be more browser-safe: only previewable MIME types render inline; others download as attachments, and responses now include nosniff.

Clicking a source in the Chainlit chat showed "Failed to load PDF file".
Two independent bugs were responsible:

1. The pdf.js worker could not load. Chainlit is submounted at /chainlit,
   but react-pdf computes its worker URL at runtime as the origin-root
   /assets/pdf.worker*.mjs (missing the /chainlit prefix). That path was
   not auth-bypassed and nothing served it, so it returned a 403 JSON body
   and the browser blocked the module worker on a forbidden MIME type -
   pdf.js never started and the PDF was never fetched.

2. /static/{chunk_id} served the file as Content-Disposition: attachment
   (FileResponse defaults to attachment when filename is set), so the
   browser downloaded it instead of rendering it inline.

Fixes:
- main.py: also serve Chainlit's frontend asset dir at /assets so the
  worker resolves with a JavaScript MIME type.
- auth.py: bypass /assets/* (public frontend bundles, the same files
  already served and bypassed under /chainlit/assets).
- download.py: serve /static inline for safe previewable media types
  (PDF/image/audio/video); HTML/SVG stay attachment to avoid a
  same-origin stored-XSS vector.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aaa59d6c-c1f5-41a6-b109-e0bb332580ab

📥 Commits

Reviewing files that changed from the base of the PR and between 779f4db and ed28fd6.

📒 Files selected for processing (1)
  • openrag/api/routers/user/download.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openrag/api/routers/user/download.py

📝 Walkthrough

Walkthrough

This PR mounts Chainlit root assets at /assets, updates auth bypasses for those requests, adds request-id middleware for token state, and changes file downloads to use MIME-based inline or attachment disposition.

Changes

Chainlit asset serving and download disposition

Layer / File(s) Summary
Chainlit root assets and request handling
openrag/api/chainlit_assets.py, openrag/api/main.py, openrag/chainlit_api.py, openrag/api/middleware/auth.py
A helper now mounts Chainlit’s root /assets directory, both app entry points call it, request-id middleware is added before Chainlit auth handling, and auth bypass logic now includes /assets/ requests.
MIME-based file disposition in download route
openrag/api/routers/user/download.py
The /static/{extract_id} route now guesses MIME type and serves safe renderable files inline, with all other types returned as attachments.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the primary change: fixing Chainlit source PDF previews in the chat side panel.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chainlit-pdf-preview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f46bee2e0b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread openrag/api/main.py Outdated
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator Author

@coderabbits revuew this PR.

@hedhoud

hedhoud commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@codex review

The source-PDF-preview fix mounted Chainlit's root-absolute /assets worker
only on the API app. In Ray Serve mode the browser-facing UI is the standalone
chainlit_api process on its own port, which lacked that mount — so
GET /assets/pdf.worker*.mjs 404'd there and previews stayed broken.

Extract the mount into a shared mount_chainlit_root_assets() helper and call it
from both api.main and chainlit_api, mirroring how chainlit_api already
replicates the /static download route for the same separate-origin reason.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
openrag/api/chainlit_assets.py (1)

27-34: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider logging when the assets directory is missing.

The no-op path is silent — if Chainlit's frontend build is missing/stale in a deployment, /assets requests will silently 404 with no diagnostic trail, which is exactly the class of hard-to-debug issue this PR is fixing. A one-line warning would help future troubleshooting.

Note: there's a documented conflict on the logger import path — the coding guidelines say from utils.logger import get_logger, but a repo learning states there is no utils.logger module and the canonical import is from core.utils.logging import get_logger. Please confirm which is currently correct before adding the log call.

💡 Proposed logging addition (pending import path confirmation)
 def mount_chainlit_root_assets(app: FastAPI) -> None:
     """Mount Chainlit's ``frontend/dist/assets`` at ``/assets`` (no-op if absent)."""
     import chainlit
     from starlette.staticfiles import StaticFiles

     assets_dir = Path(chainlit.__file__).parent / "frontend" / "dist" / "assets"
     if assets_dir.is_dir():
         app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="chainlit_root_assets")
+    else:
+        logger.warning("Chainlit frontend assets dir not found at {}; /assets mount skipped", assets_dir)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/api/chainlit_assets.py` around lines 27 - 34, The silent no-op in
mount_chainlit_root_assets should log a warning when the Chainlit assets
directory is missing so stale/missing frontend builds are diagnosable. Update
the mount_chainlit_root_assets function to add an explicit warning in the
assets_dir.is_dir() false path, using the repo-correct logger import before
emitting the message. Keep the existing mount behavior unchanged when the
directory exists, and make sure the warning clearly names the missing assets
directory.

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openrag/api/chainlit_assets.py`:
- Around line 27-34: The silent no-op in mount_chainlit_root_assets should log a
warning when the Chainlit assets directory is missing so stale/missing frontend
builds are diagnosable. Update the mount_chainlit_root_assets function to add an
explicit warning in the assets_dir.is_dir() false path, using the repo-correct
logger import before emitting the message. Keep the existing mount behavior
unchanged when the directory exists, and make sure the warning clearly names the
missing assets directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2115f93f-f3b8-47af-9bba-ec8e09e752b9

📥 Commits

Reviewing files that changed from the base of the PR and between f46bee2 and 655f703.

📒 Files selected for processing (3)
  • openrag/api/chainlit_assets.py
  • openrag/api/main.py
  • openrag/chainlit_api.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • openrag/api/main.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f46bee2e0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread openrag/api/main.py Outdated
The standalone Chainlit asset mount accessed chainlit.__file__ directly,
which raised AttributeError under the unit tests that stub chainlit as a
bare ModuleType (no __file__). Guard with getattr and no-op when absent —
a namespace package or test double simply has no bundled assets to serve.

Fixes the tests (3.12) job on PR #606.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I left one small hardening comment on the inline preview response.

Comment thread openrag/api/routers/user/download.py
Under Ray Serve the standalone chainlit_api app serves /static source-file
downloads on its own origin, but it only registered AuthMiddleware.
AuthMiddleware authenticates /static via the ?token= query param, reading it
from request.state.original_token — which only RequestIdMiddleware populates.
Without it, every source-file fetch saw token=None and returned 403 "Missing
token", breaking source previews (PDF/image/audio/video) whenever ray.serve is
enabled. Register RequestIdMiddleware outside AuthMiddleware, mirroring
api.main, so original_token is set before auth reads it.
The /static source-download route serves some files inline with a Content-Type
inferred from the filename. Add a nosniff header so the browser can't MIME-sniff
a mislabeled file into an executable type (HTML/JS) in the app origin —
defense-in-depth alongside the existing attachment fallback for HTML/SVG.

Addresses @hedhoud's review on PR #606.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tested the preview path locally against the latest head. The Chainlit worker is now served from /assets with a JavaScript content type, PDF sources render inline, and HTML/SVG stay as attachments with nosniff set. CI is green as well. Looks good to merge from my side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants