From 46d46790b518b6d99b1870fa11a9edec1f786f08 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Thu, 26 Feb 2026 21:36:03 -0500 Subject: [PATCH] Fix Android image loads by attaching auth headers (#9554) * Fix images not loading on android" * Update app/components/expo_image/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update snapshots and remove accept * Fix 16KB page size patch failing due to stale diff context (#9555) The expo_image/index.tsx hunks in 9325-full.diff were generated before the NetworkManager/header-handling code was added to ExpoImage. The stale context lines caused patch's fuzzy matching to apply ExpoImage hunks to ExpoImageBackground instead, then ExpoImageBackground hunks failed because those sections were already modified. Regenerated the expo_image diff hunks to match the current file state with proper context lines for both ExpoImage (with header handling) and ExpoImageBackground components. https://claude.ai/code/session_01SGXWQzxrcPpWsq2YrjoUwd Co-authored-by: Claude --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Your Name Co-authored-by: Mattermost Build Co-authored-by: Claude --- app/components/expo_image/index.tsx | 51 +++++++++++++++++-- .../attachment_author.test.tsx.snap | 2 + .../attachment_footer.test.tsx.snap | 1 + .../__snapshots__/index.test.tsx.snap | 4 ++ scripts/android-16kb-pagesize/9325-full.diff | 40 ++++++++------- 5 files changed, 77 insertions(+), 21 deletions(-) diff --git a/app/components/expo_image/index.tsx b/app/components/expo_image/index.tsx index 9dbffd7ca..b2723fb99 100644 --- a/app/components/expo_image/index.tsx +++ b/app/components/expo_image/index.tsx @@ -6,6 +6,7 @@ import React, {forwardRef, useMemo} from 'react'; import Animated from 'react-native-reanimated'; import {useServerUrl} from '@context/server'; +import NetworkManager from '@managers/network_manager'; import {urlSafeBase64Encode} from '@utils/security'; type ExpoImagePropsWithId = ImageProps & {id: string}; @@ -16,8 +17,36 @@ type ExpoImageBackgroundPropsWithId = ImageBackgroundProps & {id: string}; type ExpoImageBackgroundPropsMemoryOnly = ImageBackgroundProps & {cachePolicy: 'memory'; id?: string}; type ExpoImageBackgroundProps = ExpoImageBackgroundPropsWithId | ExpoImageBackgroundPropsMemoryOnly; +function shouldAttachServerAuthHeaders(uri: string | undefined, serverUrl: string) { + if (!uri) { + return false; + } + + try { + const requestUrl = new URL(uri); + const serverBaseUrl = new URL(serverUrl); + + if (requestUrl.origin !== serverBaseUrl.origin) { + return false; + } + + return requestUrl.pathname.startsWith('/api/v4/'); + } catch { + // On any parsing error, do not attach auth headers + return false; + } +} + const ExpoImage = forwardRef(({id, ...props}, ref) => { const serverUrl = useServerUrl(); + const requestHeaders = useMemo(() => { + try { + const client = NetworkManager.getClient(serverUrl); + return client.getRequestHeaders('GET'); + } catch { + return undefined; + } + }, [serverUrl]); /** * SECURITY NOTE: cachePath uses base64 encoding for URL safety, NOT encryption. @@ -30,17 +59,24 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { return props.source; } + const sourceHeaders = shouldAttachServerAuthHeaders(props.source?.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...props.source?.headers} : props.source?.headers; + delete sourceHeaders?.Accept; + // Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching) if (id) { return { ...props.source, + headers: sourceHeaders, cacheKey: id, cachePath, }; } - return props.source; - }, [id, props.source, cachePath]); + return { + ...props.source, + headers: sourceHeaders, + }; + }, [id, props.source, cachePath, requestHeaders, serverUrl]); // Process placeholder to add cachePath and cacheKey if it has a uri const placeholder: ImageSource | undefined = useMemo(() => { @@ -48,17 +84,24 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { return props.placeholder; } + const placeholderHeaders = shouldAttachServerAuthHeaders(props.placeholder?.uri, serverUrl) && requestHeaders ? {...requestHeaders, ...props.placeholder?.headers} : props.placeholder?.headers; + delete placeholderHeaders?.Accept; + // If placeholder has a uri and id is provided, add cachePath and cacheKey if (props.placeholder.uri && id) { return { ...props.placeholder, + headers: placeholderHeaders, cacheKey: `${id}-thumb`, cachePath, }; } - return props.placeholder; - }, [props.placeholder, id, cachePath]); + return { + ...props.placeholder, + headers: placeholderHeaders, + }; + }, [props.placeholder, id, cachePath, requestHeaders, serverUrl]); return ( (({id, ...props}, ref) => { +@@ -54,8 +56,8 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { * for filesystem path compatibility (avoiding special characters in directory names). */ const cachePath = useMemo(() => urlSafeBase64Encode(serverUrl), [serverUrl]); @@ -58,16 +58,19 @@ index 9dbffd7ca6..bd70fffb9c 100644 + if (typeof props.source === 'number' || typeof props.source === 'string' || Array.isArray(props.source) || !props.source) { return props.source; } - + +@@ -63,7 +65,7 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { + delete sourceHeaders?.Accept; + // Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching) - if (id) { + if (id && typeof props.source === 'object' && 'uri' in props.source) { return { ...props.source, - cacheKey: id, -@@ -43,13 +45,13 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { - }, [id, props.source, cachePath]); - + headers: sourceHeaders, +@@ -79,8 +81,8 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { + }, [id, props.source, cachePath, requestHeaders, serverUrl]); + // Process placeholder to add cachePath and cacheKey if it has a uri - const placeholder: ImageSource | undefined = useMemo(() => { - if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string') { @@ -75,14 +78,17 @@ index 9dbffd7ca6..bd70fffb9c 100644 + if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string' || Array.isArray(props.placeholder)) { return props.placeholder; } - + +@@ -88,7 +90,7 @@ const ExpoImage = forwardRef(({id, ...props}, ref) => { + delete placeholderHeaders?.Accept; + // If placeholder has a uri and id is provided, add cachePath and cacheKey - if (props.placeholder.uri && id) { + if (typeof props.placeholder === 'object' && 'uri' in props.placeholder && props.placeholder.uri && id) { return { ...props.placeholder, - cacheKey: `${id}-thumb`, -@@ -74,13 +76,13 @@ ExpoImage.displayName = 'ExpoImage'; + headers: placeholderHeaders, +@@ -117,13 +119,13 @@ ExpoImage.displayName = 'ExpoImage'; const ExpoImageBackground = ({id, ...props}: ExpoImageBackgroundProps) => { const serverUrl = useServerUrl(); const cachePath = useMemo(() => urlSafeBase64Encode(serverUrl), [serverUrl]); @@ -92,16 +98,16 @@ index 9dbffd7ca6..bd70fffb9c 100644 + if (typeof props.source === 'number' || typeof props.source === 'string' || Array.isArray(props.source) || !props.source) { return props.source; } - + // Only add cacheKey and cachePath if id is provided (i.e., not memory-only caching) - if (id) { + if (id && typeof props.source === 'object' && 'uri' in props.source) { return { ...props.source, cacheKey: id, -@@ -92,13 +94,13 @@ const ExpoImageBackground = ({id, ...props}: ExpoImageBackgroundProps) => { +@@ -135,13 +137,13 @@ const ExpoImageBackground = ({id, ...props}: ExpoImageBackgroundProps) => { }, [id, props.source, cachePath]); - + // Process placeholder to add cachePath and cacheKey if it has a uri - const placeholder: ImageSource | undefined = useMemo(() => { - if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string') { @@ -109,7 +115,7 @@ index 9dbffd7ca6..bd70fffb9c 100644 + if (!props.placeholder || typeof props.placeholder === 'number' || typeof props.placeholder === 'string' || Array.isArray(props.placeholder)) { return props.placeholder; } - + // If placeholder has a uri and id is provided, add cachePath and cacheKey - if (props.placeholder.uri && id) { + if (typeof props.placeholder === 'object' && 'uri' in props.placeholder && props.placeholder.uri && id) {