From a851228dedf67ea03697afb69c3fe97c58d41bd9 Mon Sep 17 00:00:00 2001 From: monsterDavid Date: Thu, 11 Jun 2026 00:46:20 -0700 Subject: [PATCH] fix(preview): authenticate markdown document preview requests (#15589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes [#15585](https://github.com/infiniflow/ragflow/issues/15585). - Route markdown preview through the shared `request` client (same as txt/image previewers) so `Authorization` headers and interceptors are applied consistently. - Add a unit test covering `AUTH_BETA` token loading for embedded search auth. ## Root cause Search result preview for `.md`/`.mdx` used raw `fetch`, which did not apply the same auth path as other preview types. That led to `401` on `GET /api/v1/documents/{id}/preview` even when the user was logged in or using an embedded search `auth` query param. ## Test plan - [ ] Log in, run a search, open a markdown citation link — preview loads (no 401). - [ ] Open an embedded shared search URL with `auth` query param, preview a markdown file — preview loads. - [ ] Confirm PDF/txt preview still works in the same search UI. --------- Co-authored-by: MkDev11 <89318445+bitloi@users.noreply.github.com> Co-authored-by: Wang Qi --- .../test_system_app/test_apps_init_unit.py | 29 ++++++++ .../components/document-preview/md/index.tsx | 73 +++++++++++++++---- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/test/testcases/test_web_api/test_system_app/test_apps_init_unit.py b/test/testcases/test_web_api/test_system_app/test_apps_init_unit.py index c7d951270a..0ba7ee463a 100644 --- a/test/testcases/test_web_api/test_system_app/test_apps_init_unit.py +++ b/test/testcases/test_web_api/test_system_app/test_apps_init_unit.py @@ -156,6 +156,12 @@ def test_load_user_api_token_fallback_and_fallback_exception(monkeypatch, caplog monkeypatch.setattr(apps_module.Serializer, "loads", _raise_decode) fallback_user_empty_token = SimpleNamespace(email="fallback@example.com", access_token="") + valid_token = "a" * 32 + beta_user = SimpleNamespace( + id="tenant-1", + email="embed@example.com", + access_token=valid_token, + ) async def _case(): monkeypatch.setattr(apps_module.APIToken, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-1")]) @@ -171,6 +177,29 @@ def test_load_user_api_token_fallback_and_fallback_exception(monkeypatch, caplog with caplog.at_level(logging.WARNING): assert apps_module._load_user() is None + def _query_api_token(**kwargs): + if kwargs.get("beta") == "embed-beta": + return [SimpleNamespace(tenant_id="tenant-1")] + return [] + + def _query_user(**kwargs): + if ( + kwargs.get("id") == "tenant-1" + and kwargs.get("status") == apps_module.StatusEnum.VALID.value + ): + return [beta_user] + return [] + + monkeypatch.setattr(apps_module.APIToken, "query", _query_api_token) + monkeypatch.setattr(apps_module.UserService, "query", _query_user) + async with quart_app.test_request_context("/", headers={"Authorization": "Bearer embed-beta"}): + user = apps_module._load_user(auth_types=[apps_module.AUTH_BETA]) + assert user is beta_user + assert apps_module.g.auth_type == apps_module.AUTH_BETA + + async with quart_app.test_request_context("/", headers={"Authorization": "Bearer invalid-beta"}): + assert apps_module._load_user(auth_types=[apps_module.AUTH_BETA]) is None + _run(_case()) assert "api token fallback failed" in caplog.text diff --git a/web/src/components/document-preview/md/index.tsx b/web/src/components/document-preview/md/index.tsx index 13f1af3c2f..c4c4943e15 100644 --- a/web/src/components/document-preview/md/index.tsx +++ b/web/src/components/document-preview/md/index.tsx @@ -1,8 +1,9 @@ -import { Authorization } from '@/constants/authorization'; +import message from '@/components/ui/message'; +import { Spin } from '@/components/ui/spin'; import { MarkdownRemarkPluginsLite } from '@/constants/markdown-remark-plugins'; import { cn } from '@/lib/utils'; import FileError from '@/pages/document-viewer/file-error'; -import { getAuthorization } from '@/utils/authorization-util'; +import request from '@/utils/request'; import React, { useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; @@ -15,16 +16,52 @@ interface MdProps { export const Md: React.FC = ({ url, className }) => { const [content, setContent] = useState(''); const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); useEffect(() => { + if (!url) { + setContent(''); + setError(null); + setLoading(false); + return; + } + + let cancelled = false; setError(null); - fetch(url, { headers: { [Authorization]: getAuthorization() } }) - .then((res) => { - if (!res.ok) throw new Error('Failed to fetch markdown file'); - return res.text(); - }) - .then((text) => setContent(text)) - .catch((err) => setError(err.message)); + setLoading(true); + + const fetchMarkdown = async () => { + try { + const res = await request(url, { + method: 'GET', + responseType: 'blob', + onError: (err: unknown) => { + console.error('Error loading markdown file:', err); + }, + }); + if (cancelled) return; + + const blob = res.data; + const text = await blob.text(); + if (cancelled) return; + setContent(text); + } catch (err: unknown) { + if (cancelled) return; + const messageText = + err instanceof Error ? err.message : 'Failed to fetch markdown file'; + setError(messageText); + message.error('Failed to load file'); + } finally { + if (!cancelled) { + setLoading(false); + } + } + }; + + fetchMarkdown(); + return () => { + cancelled = true; + }; }, [url]); if (error) return {error}; @@ -32,11 +69,21 @@ export const Md: React.FC = ({ url, className }) => { return (
- - {content} - + {loading && ( +
+ +
+ )} + {!loading && ( + + {content} + + )}
); };