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 <noreply@anthropic.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Your Name <larkox@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Nick Misasi 2026-02-26 21:36:03 -05:00 committed by GitHub
parent 7afdaddd2b
commit 46d46790b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 21 deletions

View file

@ -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<Image, ExpoImageProps>(({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<Image, ExpoImageProps>(({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<Image, ExpoImageProps>(({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 (
<Image

View file

@ -31,6 +31,7 @@ exports[`AttachmentAuthor it matches snapshot when both name and icon are provid
{
"cacheKey": "attachment-author-icon-aHR0cHM6Ly9pbWFnZXMuY29tL2ltYWdlLnBuZw==",
"cachePath": "",
"headers": undefined,
"uri": "https://images.com/image.png",
},
]
@ -95,6 +96,7 @@ exports[`AttachmentAuthor it matches snapshot when only icon is provided 1`] = `
{
"cacheKey": "attachment-author-icon-aHR0cHM6Ly9pbWFnZXMuY29tL2ltYWdlLnBuZw==",
"cachePath": "",
"headers": undefined,
"uri": "https://images.com/image.png",
},
]

View file

@ -33,6 +33,7 @@ exports[`AttachmentFooter it matches snapshot when both footer and footer_icon a
{
"cacheKey": "attachment-footer-icon-aHR0cHM6Ly9pbWFnZXMuY29tL2ltYWdlLnBuZw==",
"cachePath": "",
"headers": undefined,
"uri": "https://images.com/image.png",
},
]

View file

@ -809,6 +809,7 @@ exports[`components/channel_list_row should show results and tutorial 1`] = `
{
"cacheKey": "user-1-123456",
"cachePath": "",
"headers": undefined,
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
},
]
@ -1152,6 +1153,7 @@ exports[`components/channel_list_row should show results no tutorial 1`] = `
{
"cacheKey": "user-1-123456",
"cachePath": "",
"headers": undefined,
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
},
]
@ -1533,6 +1535,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
{
"cacheKey": "user-1-123456",
"cachePath": "",
"headers": undefined,
"uri": "https://community.mattermost.com/api/v4/users/1/image?_=123456",
},
]
@ -1775,6 +1778,7 @@ exports[`components/channel_list_row should show results no tutorial 2 users 1`]
{
"cacheKey": "user-2-123456",
"cachePath": "",
"headers": undefined,
"uri": "https://community.mattermost.com/api/v4/users/2/image?_=123456",
},
]

View file

@ -36,19 +36,19 @@ index 08eb45fcd9..914911c17a 100644
+ ]
}
diff --git a/app/components/expo_image/index.tsx b/app/components/expo_image/index.tsx
index 9dbffd7ca6..bd70fffb9c 100644
index b2723fb..1ed5e15 100644
--- a/app/components/expo_image/index.tsx
+++ b/app/components/expo_image/index.tsx
@@ -8,6 +8,8 @@ import Animated from 'react-native-reanimated';
import {useServerUrl} from '@context/server';
@@ -9,6 +9,8 @@ import {useServerUrl} from '@context/server';
import NetworkManager from '@managers/network_manager';
import {urlSafeBase64Encode} from '@utils/security';
+import type {SharedRefType} from 'expo';
+
type ExpoImagePropsWithId = ImageProps & {id: string};
type ExpoImagePropsMemoryOnly = ImageProps & {cachePolicy: 'memory'; id?: string};
type ExpoImageProps = ExpoImagePropsWithId | ExpoImagePropsMemoryOnly;
@@ -25,13 +27,13 @@ const ExpoImage = forwardRef<Image, ExpoImageProps>(({id, ...props}, ref) => {
@@ -54,8 +56,8 @@ const ExpoImage = forwardRef<Image, ExpoImageProps>(({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<Image, ExpoImageProps>(({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<Image, ExpoImageProps>(({id, ...props}, ref) => {
}, [id, props.source, cachePath]);
headers: sourceHeaders,
@@ -79,8 +81,8 @@ const ExpoImage = forwardRef<Image, ExpoImageProps>(({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<Image, ExpoImageProps>(({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) {