From 67ae17961e3029d9840f2f53dce41db0ee4d94e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Fri, 9 Jan 2026 15:58:34 +0100 Subject: [PATCH] Fix MS Office preview for private/local URLs (#17044) ## Summary Fixes #16900 - MS Office documents (doc, docx, ppt, pptx, xls, xlsx, odt) cannot be previewed when hosted on local/private networks. ## Problem Microsoft's Office Online viewer (`view.officeapps.live.com`) requires documents to be publicly accessible from the internet. For self-hosted Twenty instances or local development, files are not reachable by Microsoft's servers, causing the viewer to fail with "An error occurred". ## Solution Detect private/local URLs upfront and show a helpful message with a download button instead of letting the viewer fail silently. **Detected private URLs:** - `localhost`, `127.0.0.1`, `[::1]` - Private IP ranges: `10.x.x.x`, `172.16-31.x.x`, `192.168.x.x` - Local domain patterns: `.local`, `.localhost`, `.internal` ## Alternatives Explored (that don't work) We investigated more sophisticated detection approaches: 1. **postMessage events** - Microsoft's viewer doesn't send any error messages we can listen to 2. **Fetching the embed URL** - Blocked by CORS (Microsoft doesn't set `Access-Control-Allow-Origin`) 3. **Reading iframe content** - Cross-origin restrictions prevent JavaScript access The URL-based detection is the most reliable approach for catching the common cases where preview will definitely fail. ## Testing 1. Upload an Office file (docx, pptx, xlsx) on a local Twenty instance 2. Try to preview it 3. Should see "This file cannot be previewed because it is hosted locally" with a Download button --- .../files/components/DocumentViewer.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx b/packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx index 48db2a0456..3175c957a1 100644 --- a/packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/DocumentViewer.tsx @@ -13,6 +13,16 @@ import { IconDownload } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension'; +const MS_OFFICE_EXTENSIONS = [ + 'doc', + 'docx', + 'ppt', + 'pptx', + 'xls', + 'xlsx', + 'odt', +]; + const StyledDocumentViewerContainer = styled.div` display: flex; flex-direction: column; @@ -58,6 +68,11 @@ const StyledMessage = styled.div` max-width: 400px; `; +const StyledLightMessage = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + font-size: ${({ theme }) => theme.font.size.sm}; +`; + const StyledTitle = styled.div` color: ${({ theme }) => theme.font.color.primary}; font-size: ${({ theme }) => theme.font.size.xl}; @@ -69,6 +84,53 @@ type DocumentViewerProps = { documentUrl: string; }; +// MS Office Online viewer requires documents to be publicly accessible from the internet. +// For private/local URLs, Microsoft's servers cannot fetch the document. +// +// We explored more sophisticated detection approaches but they don't work: +// - postMessage: Microsoft's viewer doesn't send any error messages +// - Fetching the embed URL: Blocked by CORS (no Access-Control-Allow-Origin header) +// - Reading iframe content: Cross-origin restrictions prevent access +// +// This simple URL-based detection catches the most common cases (localhost, private IPs) +// where the preview will definitely fail. For edge cases on non-standard private networks, +// users will see Microsoft's error page but can still download the file. +// +// See: https://github.com/twentyhq/twenty/issues/16900 +const isPrivateUrl = (url: string): boolean => { + try { + const { hostname } = new URL(url); + + if ( + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname === '[::1]' + ) { + return true; + } + + const ipParts = hostname.split('.').map(Number); + if (ipParts.length === 4 && ipParts.every((part) => !isNaN(part))) { + if (ipParts[0] === 10) return true; + if (ipParts[0] === 172 && ipParts[1] >= 16 && ipParts[1] <= 31) + return true; + if (ipParts[0] === 192 && ipParts[1] === 168) return true; + } + + if ( + hostname.endsWith('.local') || + hostname.endsWith('.localhost') || + hostname.endsWith('.internal') + ) { + return true; + } + + return false; + } catch { + return false; + } +}; + const MIME_TYPE_MAPPING: Record< (typeof PREVIEWABLE_EXTENSIONS)[number], string @@ -107,6 +169,7 @@ export const DocumentViewer = ({ const fileExtension = extension?.toLowerCase().replace('.', '') ?? ''; const fileCategory = getFileType(documentName); const isPreviewable = PREVIEWABLE_EXTENSIONS.includes(fileExtension); + const isMsOfficeFile = MS_OFFICE_EXTENSIONS.includes(fileExtension); const mimeType = PREVIEWABLE_EXTENSIONS.includes(fileExtension) ? MIME_TYPE_MAPPING[fileExtension] @@ -158,6 +221,27 @@ export const DocumentViewer = ({ ); + if (isMsOfficeFile && isPrivateUrl(documentUrl)) { + return ( + + + + + This file cannot be previewed because it is hosted locally. + + +