From 84e5cead089ed830ab9001dc1bdd1d7b25b2d918 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Fri, 24 Apr 2026 23:13:24 +0200 Subject: [PATCH] fix: pre-fetch bincio wheel via RN networking to avoid ATS blocking HTTP in WKWebView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WKWebView blocks HTTP requests (ATS) even when NSAllowsLocalNetworking is set for the app's own networking — so fetch(http://192.168.x.x/api/wheel/download) inside the WebView always fails with 'Load failed' on iOS. - extractActivity.ts: rename wheelUrl param to wheelBase64; WebView now receives the wheel as pre-fetched base64 bytes rather than a URL to fetch itself - PyodideWebView.tsx: decode wheelBase64 → Uint8Array → Blob → blob URL for micropip.install; fix baseUrl '' → 'https://localhost' (null origin blocks fetch on iOS) - import.tsx: add fetchWheelBase64() that resolves the wheel URL via /api/wheel/version then fetches with native networking (HTTP works); caches result in memory so repeated imports in one session don't re-download --- mobile/app/(tabs)/import.tsx | 43 +++++++++++++++++++--------- mobile/extraction/PyodideWebView.tsx | 21 +++++++------- mobile/extraction/extractActivity.ts | 6 ++-- 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/mobile/app/(tabs)/import.tsx b/mobile/app/(tabs)/import.tsx index 0c6458e..beb99dc 100644 --- a/mobile/app/(tabs)/import.tsx +++ b/mobile/app/(tabs)/import.tsx @@ -91,14 +91,17 @@ export default function ImportScreen() { encoding: FileSystem.EncodingType.Base64, }); - // Wheel URL: prefer the configured instance; fall back to bincio.org. + // Fetch the bincio wheel here (React Native networking), not inside the + // WebView. WKWebView blocks HTTP requests via ATS; RN native networking + // allows local-network HTTP (NSAllowsLocalNetworking=true in Info.plist). const instanceUrl = await getInstanceUrl(dbCtx); - const wheelUrl = await resolveWheelUrl(instanceUrl); + setState({ status: 'loading', msg: 'Fetching Bincio engine…' }); + const wheelBase64 = await fetchWheelBase64(instanceUrl); const result = await extractFile( name, base64, - wheelUrl, + wheelBase64, (msg) => setState({ status: 'loading', msg }), ); @@ -146,11 +149,9 @@ export default function ImportScreen() { {state.status === 'loading' && ( {state.msg} - {state.msg.startsWith('Load') && ( - - First run downloads ~35 MB of the Python runtime. Subsequent runs are instant. - - )} + + First run downloads ~35 MB (Python runtime + packages). Subsequent runs are instant. + )} @@ -204,17 +205,31 @@ async function getInstanceUrl(db: ReturnType): Promise< return (row?.value ?? '').replace(/\/$/, ''); } -async function resolveWheelUrl(instanceUrl: string): Promise { +// In-memory cache so repeated imports in one session don't re-download the wheel. +let _cachedWheelBase64: string | null = null; + +async function fetchWheelBase64(instanceUrl: string): Promise { + if (_cachedWheelBase64) return _cachedWheelBase64; + const base = instanceUrl || 'https://bincio.org'; + + // Ask the instance for the canonical wheel URL (handles both dev and prod layouts). + let wheelUrl = `${base}/api/wheel/download`; try { - const resp = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); - if (resp.ok) { - const d = await resp.json() as { api_url?: string; url?: string }; + const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) }); + if (vr.ok) { + const d = await vr.json() as { api_url?: string; url?: string }; const path = d.api_url ?? d.url ?? '/api/wheel/download'; - return path.startsWith('http') ? path : `${base}${path}`; + wheelUrl = path.startsWith('http') ? path : `${base}${path}`; } } catch {} - return `${base}/api/wheel/download`; + + // Fetch via React Native networking (supports local HTTP; WKWebView would block it). + const resp = await fetch(wheelUrl); + if (!resp.ok) throw new Error(`Could not download Bincio engine (${resp.status}). Is the instance running?`); + const buf = await resp.arrayBuffer(); + _cachedWheelBase64 = Buffer.from(buf).toString('base64'); + return _cachedWheelBase64; } // ── Styles ─────────────────────────────────────────────────────────────────── diff --git a/mobile/extraction/PyodideWebView.tsx b/mobile/extraction/PyodideWebView.tsx index f430e06..222ef8b 100644 --- a/mobile/extraction/PyodideWebView.tsx +++ b/mobile/extraction/PyodideWebView.tsx @@ -87,10 +87,10 @@ var initError = null; })(); window._bincioExtract = async function(params) { - var reqId = params.reqId; - var filename = params.filename; - var base64 = params.base64; - var wheelUrl = params.wheelUrl; + var reqId = params.reqId; + var filename = params.filename; + var base64 = params.base64; + var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues) function post(m) { _post(Object.assign({}, m, { reqId: reqId })); } @@ -108,13 +108,14 @@ window._bincioExtract = async function(params) { } if (initError) throw new Error(initError); - // Install bincio wheel on first extraction (lazy: keeps startup fast) + // Install bincio wheel on first extraction. + // Wheel bytes arrive pre-fetched from the React Native side as base64, + // so the WebView never needs to make an HTTP request (avoids ATS blocks). if (!wheelReady) { post({ type: 'progress', msg: 'Loading Bincio…' }); - var resp = await fetch(wheelUrl); - if (!resp.ok) throw new Error('Failed to fetch Bincio wheel (' + resp.status + ')'); - var wheelBlob = new Blob([await resp.arrayBuffer()]); - var blobUrl = URL.createObjectURL(wheelBlob); + var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); }); + var wheelBlob = new Blob([wheelBytes]); + var blobUrl = URL.createObjectURL(wheelBlob); pyodide.globals.set('_blobUrl', blobUrl); await pyodide.runPythonAsync(_PY_INSTALL_WHEEL); URL.revokeObjectURL(blobUrl); @@ -158,7 +159,7 @@ export function PyodideWebView() { return ( void = () => {}, ): Promise { if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress')); @@ -67,7 +69,7 @@ export function extractFile( isExtracting = true; const reqId = String(++reqCounter); - const args = JSON.stringify({ reqId, filename, base64, wheelUrl }); + const args = JSON.stringify({ reqId, filename, base64, wheelBase64 }); return new Promise((resolve, reject) => { pending.set(reqId, {