From bcea5673c3a616fb794d6d812e55055eec1b8193 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Sun, 8 Mar 2026 15:29:52 -0700 Subject: [PATCH 1/2] Fix: Use initialized mix fetch instance on Android The initMixFetch() function returns a properly initialized IMixFetch instance, but the code was discarding it and calling the raw mixFetch() function instead. On Android, the raw function uses an uninitialized WASM context, causing a panic. Fix: Store the initialized instance and pass it to queueMixFetch(), which now accepts an optional IMixFetch parameter. Falls back to the raw function if no instance is provided for compatibility. Fixes the 'panic: JavaScript error: mix fetch hasn'\''t been initialised' error on Android while keeping it working on iOS. --- src/io/browser/browser-io.ts | 14 +++++++++----- src/io/react-native/react-native-worker.ts | 14 +++++++++----- src/util/nym.ts | 12 ++++++++++-- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/io/browser/browser-io.ts b/src/io/browser/browser-io.ts index 80fa556e8..a1984bf82 100644 --- a/src/io/browser/browser-io.ts +++ b/src/io/browser/browser-io.ts @@ -50,12 +50,16 @@ export function makeBrowserIo(logBackend: LogBackend): EdgeIo { if (privacy === 'nym') { // Ensure mixFetch is initialized before use - await initMixFetch(log) + const nymMixFetch = await initMixFetch(log) // Use queued fetch to handle mixFetch's one-request-per-host limitation - return await queueMixFetch(uri, { - ...opts, - mode: 'unsafe-ignore-cors' as RequestMode - }) + return await queueMixFetch( + uri, + { + ...opts, + mode: 'unsafe-ignore-cors' as RequestMode + }, + nymMixFetch + ) } if (corsBypass === 'always') { return await fetchCorsProxy(uri, opts) diff --git a/src/io/react-native/react-native-worker.ts b/src/io/react-native/react-native-worker.ts index a71b31a18..f5a195d1e 100644 --- a/src/io/react-native/react-native-worker.ts +++ b/src/io/react-native/react-native-worker.ts @@ -177,12 +177,16 @@ async function makeIo(logBackend: LogBackend): Promise { if (privacy === 'nym') { // Ensure mixFetch is initialized before use - await initMixFetch(log) + const nymMixFetch = await initMixFetch(log) // Use queued fetch to handle mixFetch's one-request-per-host limitation - const response = await queueMixFetch(uri, { - ...opts, - mode: 'unsafe-ignore-cors' as RequestMode - }) + const response = await queueMixFetch( + uri, + { + ...opts, + mode: 'unsafe-ignore-cors' as RequestMode + }, + nymMixFetch + ) return response } if (corsBypass === 'always') { diff --git a/src/util/nym.ts b/src/util/nym.ts index 23faad2d5..673cf6504 100644 --- a/src/util/nym.ts +++ b/src/util/nym.ts @@ -69,7 +69,8 @@ export async function initMixFetch(log: EdgeLog): Promise { */ export async function queueMixFetch( uri: string, - opts: RequestInit & { mode?: string } + opts: RequestInit & { mode?: string }, + instance?: IMixFetch ): Promise { const hostKey = getHostKey(uri) @@ -79,7 +80,14 @@ export async function queueMixFetch( // Chain our request after the previous one const ourWork = previousChain .catch(() => {}) // Ignore errors from previous request - .then(async () => await mixFetch(uri, opts, mixFetchOptions)) + .then(async () => { + // Use the provided instance if available (properly initialized context) + // Otherwise fall back to the module-level function + if (instance != null) { + return await instance(uri, opts, mixFetchOptions) + } + return await mixFetch(uri, opts, mixFetchOptions) + }) .finally(() => { // Clean up if we're still the chain tail if (hostRequestChains.get(hostKey) === ourWork) { From 3813e457694bf7bf31392b825fa982d2c3c89323 Mon Sep 17 00:00:00 2001 From: Sam Holmes Date: Mon, 9 Mar 2026 12:30:54 -0700 Subject: [PATCH 2/2] fixup! Fix: Use initialized mix fetch instance on Android Instead of passing the initialized instance as a parameter to queueMixFetch, let the function handle initialization internally. This keeps the module state encapsulated and simplifies the API. queueMixFetch now: - Takes log parameter (required for initialization) - Calls initMixFetch(log) internally (cached for subsequent calls) - Uses the initialized instance directly Addresses review feedback: 'Instead of a parameter for dependency injection, just initialize the instance within the nym.ts module. Have the queue function await the single initialization promise.' --- src/io/browser/browser-io.ts | 7 +++---- src/io/react-native/react-native-worker.ts | 7 +++---- src/util/nym.ts | 11 ++++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/io/browser/browser-io.ts b/src/io/browser/browser-io.ts index a1984bf82..de399f664 100644 --- a/src/io/browser/browser-io.ts +++ b/src/io/browser/browser-io.ts @@ -3,7 +3,7 @@ import { makeLocalStorageDisklet } from 'disklet' import { LogBackend, makeLog } from '../../core/log/log' import { EdgeFetchOptions, EdgeFetchResponse, EdgeIo } from '../../types/types' import { scrypt } from '../../util/crypto/scrypt' -import { initMixFetch, queueMixFetch } from '../../util/nym' +import { queueMixFetch } from '../../util/nym' import { fetchCorsProxy } from './fetch-cors-proxy' // Only try CORS proxy/bridge techniques up to 5 times @@ -49,16 +49,15 @@ export function makeBrowserIo(logBackend: LogBackend): EdgeIo { const { corsBypass = 'auto', privacy = 'none' } = opts ?? {} if (privacy === 'nym') { - // Ensure mixFetch is initialized before use - const nymMixFetch = await initMixFetch(log) // Use queued fetch to handle mixFetch's one-request-per-host limitation + // initMixFetch is called within queueMixFetch return await queueMixFetch( uri, { ...opts, mode: 'unsafe-ignore-cors' as RequestMode }, - nymMixFetch + log ) } if (corsBypass === 'always') { diff --git a/src/io/react-native/react-native-worker.ts b/src/io/react-native/react-native-worker.ts index f5a195d1e..86be60095 100644 --- a/src/io/react-native/react-native-worker.ts +++ b/src/io/react-native/react-native-worker.ts @@ -17,7 +17,7 @@ import { EdgeFetchResponse, EdgeIo } from '../../types/types' -import { initMixFetch, queueMixFetch } from '../../util/nym' +import { queueMixFetch } from '../../util/nym' import { hideProperties } from '../hidden-properties' import { makeNativeBridge } from './native-bridge' import { WorkerApi, YAOB_THROTTLE_MS } from './react-native-types' @@ -176,16 +176,15 @@ async function makeIo(logBackend: LogBackend): Promise { const { corsBypass = 'auto', privacy = 'none' } = opts ?? {} if (privacy === 'nym') { - // Ensure mixFetch is initialized before use - const nymMixFetch = await initMixFetch(log) // Use queued fetch to handle mixFetch's one-request-per-host limitation + // initMixFetch is called within queueMixFetch const response = await queueMixFetch( uri, { ...opts, mode: 'unsafe-ignore-cors' as RequestMode }, - nymMixFetch + log ) return response } diff --git a/src/util/nym.ts b/src/util/nym.ts index 673cf6504..8e700ca78 100644 --- a/src/util/nym.ts +++ b/src/util/nym.ts @@ -70,7 +70,7 @@ export async function initMixFetch(log: EdgeLog): Promise { export async function queueMixFetch( uri: string, opts: RequestInit & { mode?: string }, - instance?: IMixFetch + log: EdgeLog ): Promise { const hostKey = getHostKey(uri) @@ -81,12 +81,9 @@ export async function queueMixFetch( const ourWork = previousChain .catch(() => {}) // Ignore errors from previous request .then(async () => { - // Use the provided instance if available (properly initialized context) - // Otherwise fall back to the module-level function - if (instance != null) { - return await instance(uri, opts, mixFetchOptions) - } - return await mixFetch(uri, opts, mixFetchOptions) + // Initialize mixFetch on first use (cached for subsequent calls) + const nymMixFetch = await initMixFetch(log) + return await nymMixFetch(uri, opts, mixFetchOptions) }) .finally(() => { // Clean up if we're still the chain tail