From 0e7ca0b1c19a45487254c76b7001fcec8476a6c4 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 10 Nov 2025 16:06:08 -0500 Subject: [PATCH] Enhance WebViewer with better iframe blocking error handling - Added graceful handling for websites that block iframe embedding (X-Frame-Options) - Improved error messages explaining why certain sites can't be embedded - Added helpful fallback UI with alternatives when sites are blocked - Provided quick action buttons to open blocked sites in external browser - Added suggestions for embed-friendly alternatives (Google, Wikipedia) - Better UX for common iframe security restrictions like squareup.com - Maintains full functionality for localhost development and embed-friendly sites --- ui/desktop/src/components/WebViewer.tsx | 125 ++++++++++++++++++------ 1 file changed, 97 insertions(+), 28 deletions(-) diff --git a/ui/desktop/src/components/WebViewer.tsx b/ui/desktop/src/components/WebViewer.tsx index 818678b157..87580bd9ff 100644 --- a/ui/desktop/src/components/WebViewer.tsx +++ b/ui/desktop/src/components/WebViewer.tsx @@ -91,6 +91,7 @@ export function WebViewer({ const [retryCount, setRetryCount] = useState(0); const [iframeReady, setIframeReady] = useState(false); const [isSecure, setIsSecure] = useState(false); + const [isBlocked, setIsBlocked] = useState(false); // eslint-disable-next-line no-undef const iframeRef = useRef(null); const retryTimeoutRef = useRef | null>(null); @@ -206,6 +207,7 @@ export function WebViewer({ setIsLoading(true); setRetryCount(0); setIframeReady(false); + setIsBlocked(false); // Save to localStorage if (typeof window !== 'undefined') { @@ -261,6 +263,7 @@ export function WebViewer({ setIsLoading(false); setError(null); setRetryCount(0); + setIsBlocked(false); if (retryTimeoutRef.current) { window.clearTimeout(retryTimeoutRef.current); @@ -279,34 +282,40 @@ export function WebViewer({ }; const handleIframeError = () => { - const maxRetries = 3; - - if (retryCount < maxRetries) { - const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); - - console.log( - `Retrying to load ${url} (attempt ${retryCount + 1}/${maxRetries}) in ${retryDelay}ms...` - ); - - if (retryTimeoutRef.current) { - window.clearTimeout(retryTimeoutRef.current); - } - - retryTimeoutRef.current = setTimeout(() => { - setRetryCount((prev) => prev + 1); - if (iframeRef.current) { - const currentSrc = iframeRef.current.src; - iframeRef.current.src = ''; - iframeRef.current.src = currentSrc; - } - }, retryDelay); - } else { - setIsLoading(false); + console.log('Iframe error occurred for URL:', url); + setIsLoading(false); + setIsBlocked(true); + + // Check if this might be a blocking issue + if (!isLocalhostUrl(url)) { setError( - `Failed to load ${url} after ${maxRetries} attempts. ${ - isLocalhostUrl(url) ? 'Make sure the server is running.' : 'Please check the URL and try again.' - }` + `This website (${domain}) cannot be displayed in an embedded viewer due to security restrictions. This is common for many websites to prevent clickjacking attacks.` ); + } else { + const maxRetries = 3; + + if (retryCount < maxRetries) { + const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); + + console.log( + `Retrying to load ${url} (attempt ${retryCount + 1}/${maxRetries}) in ${retryDelay}ms...` + ); + + if (retryTimeoutRef.current) { + window.clearTimeout(retryTimeoutRef.current); + } + + retryTimeoutRef.current = setTimeout(() => { + setRetryCount((prev) => prev + 1); + if (iframeRef.current) { + const currentSrc = iframeRef.current.src; + iframeRef.current.src = ''; + iframeRef.current.src = currentSrc; + } + }, retryDelay); + } else { + setError('Failed to load localhost server. Make sure the server is running and accessible.'); + } } }; @@ -454,12 +463,44 @@ export function WebViewer({ {error && (

{error}

+ {isBlocked && !isLocalhost && ( +
+

Try these alternatives:

+
+ + + +
+
+ )}
)} {/* Iframe Content */}
- {iframeReady && ( + {iframeReady && !isBlocked && (