diff --git a/apps/web/components/document-cards/file-preview.tsx b/apps/web/components/document-cards/file-preview.tsx
index 6013a203..9965d9be 100644
--- a/apps/web/components/document-cards/file-preview.tsx
+++ b/apps/web/components/document-cards/file-preview.tsx
@@ -1,6 +1,6 @@
"use client"
-import { memo, useState } from "react"
+import { memo, useCallback, useState } from "react"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/lib/fonts"
@@ -49,6 +49,7 @@ export const FilePreview = memo(function FilePreview({
document: DocumentWithMemories
}) {
const [imageError, setImageError] = useState(false)
+ const [retryKey, setRetryKey] = useState(0)
const { extension, color } = getFileTypeInfo(document)
const type = document.type?.toLowerCase()
@@ -58,6 +59,17 @@ export const FilePreview = memo(function FilePreview({
document.url &&
!imageError
+ // On first failure, wait briefly then force a re-render with a new key to
+ // retry the fetch (covers transient R2 timing issues).
+ // On second failure, give up and show the fallback file icon view.
+ const handleImageError = useCallback(() => {
+ if (retryKey === 0) {
+ setTimeout(() => setRetryKey(1), 500)
+ return
+ }
+ setImageError(true)
+ }, [retryKey])
+
return (
{color && (
@@ -80,10 +92,11 @@ export const FilePreview = memo(function FilePreview({
/>

setImageError(true)}
+ onError={handleImageError}
loading="lazy"
/>
diff --git a/apps/web/components/document-modal/content/image-preview.tsx b/apps/web/components/document-modal/content/image-preview.tsx
index 31d5c1c0..cfa200de 100644
--- a/apps/web/components/document-modal/content/image-preview.tsx
+++ b/apps/web/components/document-modal/content/image-preview.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState } from "react"
+import { useCallback, useState } from "react"
import { cn } from "@lib/utils"
interface ImagePreviewProps {
@@ -11,6 +11,19 @@ interface ImagePreviewProps {
export function ImagePreview({ url, title }: ImagePreviewProps) {
const [imageError, setImageError] = useState(false)
const [isLoading, setIsLoading] = useState(true)
+ const [retryKey, setRetryKey] = useState(0)
+
+ // On first failure, wait briefly then force a re-render with a new key to
+ // retry the fetch (covers transient R2 timing issues).
+ // On second failure, give up and show the error state.
+ const handleImageError = useCallback(() => {
+ if (retryKey === 0) {
+ setTimeout(() => setRetryKey(1), 500)
+ return
+ }
+ setImageError(true)
+ setIsLoading(false)
+ }, [retryKey])
if (imageError || !url) {
return (
@@ -38,16 +51,14 @@ export function ImagePreview({ url, title }: ImagePreviewProps) {
/>
{
- setImageError(true)
- setIsLoading(false)
- }}
+ onError={handleImageError}
onLoad={() => setIsLoading(false)}
loading="lazy"
/>
diff --git a/apps/web/components/document-modal/content/pdf.tsx b/apps/web/components/document-modal/content/pdf.tsx
index a025cf61..a4b896ac 100644
--- a/apps/web/components/document-modal/content/pdf.tsx
+++ b/apps/web/components/document-modal/content/pdf.tsx
@@ -1,7 +1,7 @@
"use client"
import { Document, Page, pdfjs } from "react-pdf"
-import { useState } from "react"
+import { useCallback, useState } from "react"
import "react-pdf/dist/Page/AnnotationLayer.css"
import "react-pdf/dist/Page/TextLayer.css"
@@ -19,6 +19,7 @@ export function PdfViewer({ url }: PdfViewerProps) {
const [numPages, setNumPages] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
+ const [retryKey, setRetryKey] = useState(0)
if (!url) {
return (
@@ -34,10 +35,24 @@ export function PdfViewer({ url }: PdfViewerProps) {
setError(null)
}
- function onDocumentLoadError(error: Error) {
- setError(error.message || "Failed to load PDF")
- setLoading(false)
- }
+ // On first failure, wait briefly then force a re-mount of the Document
+ // component to retry (covers transient R2 timing issues).
+ // On second failure, give up and show the error state.
+ const onDocumentLoadError = useCallback(
+ (err: Error) => {
+ if (retryKey === 0) {
+ setTimeout(() => {
+ setRetryKey(1)
+ setLoading(true)
+ setError(null)
+ }, 500)
+ return
+ }
+ setError(err.message || "Failed to load PDF")
+ setLoading(false)
+ },
+ [retryKey],
+ )
return (
@@ -53,6 +68,7 @@ export function PdfViewer({ url }: PdfViewerProps) {
)}
{document.url &&
- !document.url.includes("files.supermemory.ai") &&
+ !isSupermemoryFileUrl(document.url) &&
(document.title ||
(!document.url.includes("x.com") &&
!document.url.includes("twitter.com"))) && (
diff --git a/apps/web/lib/url-helpers.ts b/apps/web/lib/url-helpers.ts
index d171a6c2..832b8722 100644
--- a/apps/web/lib/url-helpers.ts
+++ b/apps/web/lib/url-helpers.ts
@@ -189,6 +189,29 @@ export function toLinkedInProfileUrl(handle: string): string {
return `https://linkedin.com/in/${handle.trim()}`
}
+/**
+ * Checks if a URL points to a supermemory-hosted file.
+ * Matches the public bucket domain (files.supermemory.ai) and
+ * presigned R2 URLs whose hostname ends with `.r2.cloudflarestorage.com`.
+ *
+ * Note: The R2 check is intentionally broad — it matches any Cloudflare R2
+ * presigned URL, not only supermemory's account. This is acceptable because
+ * the function is only called on `document.url` values returned by our own
+ * backend, where all R2 URLs originate from the supermemory bucket.
+ * If user-supplied external R2 URLs ever appear in this field, tighten the
+ * check by also validating the account-id subdomain or the bucket path prefix.
+ */
+export const isSupermemoryFileUrl = (url: string): boolean => {
+ try {
+ const parsed = new URL(url)
+ if (parsed.hostname === "files.supermemory.ai") return true
+ if (parsed.hostname.endsWith(".r2.cloudflarestorage.com")) return true
+ return false
+ } catch {
+ return false
+ }
+}
+
/**
* Gets the favicon URL for a given URL.
*/