Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/fix-fetch-image-blob-arraybuffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@reown/appkit-core-react-native': patch
---

fix(core): load wallet & network images on React Native

`FetchUtil.fetchImage` built its data URL with `response.blob()` + `FileReader`,
but on React Native `fetch(...).blob()` throws "Creating blobs from 'ArrayBuffer'
and 'ArrayBufferView' are not supported" for binary responses — so wallet and
network images silently failed to load (placeholders only). Read the bytes via
`response.arrayBuffer()` and base64-encode them into the data URL instead, using
a dependency-free encoder (RN guarantees neither `Buffer` nor `btoa`).
59 changes: 59 additions & 0 deletions packages/core/src/__tests__/utils/FetchUtil.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,63 @@ describe('FetchUtil', () => {
expect(url).toBe('https://another.com/test?foo=bar&bar=baz&clientId=test-client-id');
});
});

describe('fetchImage', () => {
const originalFetch = global.fetch;

afterEach(() => {
global.fetch = originalFetch;
});

it('should build a base64 data URL from the response ArrayBuffer', async () => {
// "foobar" -> base64 "Zm9vYmFy"
const bytes = new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]);
global.fetch = jest.fn().mockResolvedValue({
arrayBuffer: async () => bytes.buffer,
headers: { get: () => 'image/png' }
}) as unknown as typeof fetch;

const fetchUtil = new FetchUtil({ baseUrl });
const result = await fetchUtil.fetchImage('/getWalletImage/1');

expect(result).toBe('data:image/png;base64,Zm9vYmFy');
});

it('should default the content type to image/png when the header is missing', async () => {
global.fetch = jest.fn().mockResolvedValue({
arrayBuffer: async () => new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]).buffer,
headers: { get: () => null }
}) as unknown as typeof fetch;

const fetchUtil = new FetchUtil({ baseUrl });
const result = await fetchUtil.fetchImage('/getWalletImage/1');

expect(result).toBe('data:image/png;base64,Zm9vYmFy');
});

it('should not call response.blob() (RN cannot build a Blob from an ArrayBuffer)', async () => {
const blob = jest.fn();
global.fetch = jest.fn().mockResolvedValue({
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
headers: { get: () => 'image/webp' },
blob
}) as unknown as typeof fetch;

const fetchUtil = new FetchUtil({ baseUrl });
await fetchUtil.fetchImage('/getWalletImage/1');

expect(blob).not.toHaveBeenCalled();
});

it('should return undefined when the request throws', async () => {
global.fetch = jest
.fn()
.mockRejectedValue(new Error('network error')) as unknown as typeof fetch;

const fetchUtil = new FetchUtil({ baseUrl });
const result = await fetchUtil.fetchImage('/getWalletImage/1');

expect(result).toBeUndefined();
});
});
});
37 changes: 31 additions & 6 deletions packages/core/src/utils/FetchUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,43 @@ export class FetchUtil {
try {
const url = this.createUrl({ path, params }).toString();
const response = await fetch(url, { headers });
const blob = await response.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);

return new Promise<string>(resolve => {
reader.onloadend = () => resolve(reader.result as string);
});
// React Native's `fetch(...).blob()` throws "Creating blobs from
// 'ArrayBuffer' and 'ArrayBufferView' are not supported" for binary
// responses, so Blob + FileReader can't be used to build the data URL
// (wallet/network images would silently fail to load). Read the bytes as
// an ArrayBuffer and base64-encode them into a data URL instead.
const arrayBuffer = await response.arrayBuffer();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Auto Review Issue: fetchImage encodes error-response bodies as image data URLs

Severity: MEDIUM
Category: correctness
Tool: Claude Auto Review

Context:

  • Pattern: fetchImage calls response.arrayBuffer() without first checking response.ok. processResponse (used by get/post/etc.) does guard with response.ok; fetchImage is inconsistent.
  • Risk: For 4xx/5xx responses the error body is base64-encoded and returned as a data URL (e.g. data:application/json;base64,...) instead of undefined. The catch block never fires because arrayBuffer() itself succeeds on non-ok responses.
  • Impact: Callers receive a syntactically valid but semantically garbage data URL. The RN Image component attempts (and silently fails) to render it rather than falling back to the placeholder as intended.
  • Trigger: Any image URL returning a non-2xx status (CDN 404, rate-limit 429, etc.).

Recommendation:

const response = await fetch(url, { headers });
if (!response.ok) {
  return undefined;
}
const arrayBuffer = await response.arrayBuffer();

Fix this →

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this is valid. fetchImage does not guard on response.ok, so a 4xx/5xx would get base64-encoded into a data URL and RN's Image would fail silently instead of falling back to the placeholder.

Note this is not a regression — the previous blob() version had the same gap — but since we're rewriting this block it's the right place to fix it. Will add the if (!response.ok) return undefined; guard before arrayBuffer() (plus a test).

const contentType = response.headers.get('content-type') ?? 'image/png';

return `data:${contentType};base64,${FetchUtil._arrayBufferToBase64(arrayBuffer)}`;
} catch {
return undefined;
}
}

// Dependency-free base64 encoder (RN has no global `Buffer`/`btoa` guarantee).
/* eslint-disable no-bitwise */
private static _arrayBufferToBase64(buffer: ArrayBuffer): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const bytes = new Uint8Array(buffer);
let base64 = '';

for (let i = 0; i < bytes.length; i += 3) {
const byte0 = bytes[i] as number;
const byte1 = i + 1 < bytes.length ? (bytes[i + 1] as number) : 0;
const byte2 = i + 2 < bytes.length ? (bytes[i + 2] as number) : 0;

base64 += chars[byte0 >> 2];
base64 += chars[((byte0 & 0x03) << 4) | (byte1 >> 4)];
base64 += i + 1 < bytes.length ? chars[((byte1 & 0x0f) << 2) | (byte2 >> 6)] : '=';
base64 += i + 2 < bytes.length ? chars[byte2 & 0x3f] : '=';
}

return base64;
}
/* eslint-enable no-bitwise */

public createUrl({ path, params }: RequestArguments) {
let fullUrl: string;

Expand Down
Loading