fix: pre-fetch bincio wheel via RN networking to avoid ATS blocking HTTP in WKWebView
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
This commit is contained in:
@@ -91,14 +91,17 @@ export default function ImportScreen() {
|
|||||||
encoding: FileSystem.EncodingType.Base64,
|
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 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(
|
const result = await extractFile(
|
||||||
name,
|
name,
|
||||||
base64,
|
base64,
|
||||||
wheelUrl,
|
wheelBase64,
|
||||||
(msg) => setState({ status: 'loading', msg }),
|
(msg) => setState({ status: 'loading', msg }),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,11 +149,9 @@ export default function ImportScreen() {
|
|||||||
{state.status === 'loading' && (
|
{state.status === 'loading' && (
|
||||||
<View style={styles.statusBox}>
|
<View style={styles.statusBox}>
|
||||||
<Text style={styles.statusMsg}>{state.msg}</Text>
|
<Text style={styles.statusMsg}>{state.msg}</Text>
|
||||||
{state.msg.startsWith('Load') && (
|
|
||||||
<Text style={styles.statusHint}>
|
<Text style={styles.statusHint}>
|
||||||
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.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -204,17 +205,31 @@ async function getInstanceUrl(db: ReturnType<typeof useSQLiteContext>): Promise<
|
|||||||
return (row?.value ?? '').replace(/\/$/, '');
|
return (row?.value ?? '').replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveWheelUrl(instanceUrl: string): Promise<string> {
|
// 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<string> {
|
||||||
|
if (_cachedWheelBase64) return _cachedWheelBase64;
|
||||||
|
|
||||||
const base = instanceUrl || 'https://bincio.org';
|
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 {
|
try {
|
||||||
const resp = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) });
|
const vr = await fetch(`${base}/api/wheel/version`, { signal: AbortSignal.timeout(5000) });
|
||||||
if (resp.ok) {
|
if (vr.ok) {
|
||||||
const d = await resp.json() as { api_url?: string; url?: string };
|
const d = await vr.json() as { api_url?: string; url?: string };
|
||||||
const path = d.api_url ?? d.url ?? '/api/wheel/download';
|
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 {}
|
} 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 ───────────────────────────────────────────────────────────────────
|
// ── Styles ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ window._bincioExtract = async function(params) {
|
|||||||
var reqId = params.reqId;
|
var reqId = params.reqId;
|
||||||
var filename = params.filename;
|
var filename = params.filename;
|
||||||
var base64 = params.base64;
|
var base64 = params.base64;
|
||||||
var wheelUrl = params.wheelUrl;
|
var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues)
|
||||||
|
|
||||||
function post(m) { _post(Object.assign({}, m, { reqId: reqId })); }
|
function post(m) { _post(Object.assign({}, m, { reqId: reqId })); }
|
||||||
|
|
||||||
@@ -108,12 +108,13 @@ window._bincioExtract = async function(params) {
|
|||||||
}
|
}
|
||||||
if (initError) throw new Error(initError);
|
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) {
|
if (!wheelReady) {
|
||||||
post({ type: 'progress', msg: 'Loading Bincio…' });
|
post({ type: 'progress', msg: 'Loading Bincio…' });
|
||||||
var resp = await fetch(wheelUrl);
|
var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); });
|
||||||
if (!resp.ok) throw new Error('Failed to fetch Bincio wheel (' + resp.status + ')');
|
var wheelBlob = new Blob([wheelBytes]);
|
||||||
var wheelBlob = new Blob([await resp.arrayBuffer()]);
|
|
||||||
var blobUrl = URL.createObjectURL(wheelBlob);
|
var blobUrl = URL.createObjectURL(wheelBlob);
|
||||||
pyodide.globals.set('_blobUrl', blobUrl);
|
pyodide.globals.set('_blobUrl', blobUrl);
|
||||||
await pyodide.runPythonAsync(_PY_INSTALL_WHEEL);
|
await pyodide.runPythonAsync(_PY_INSTALL_WHEEL);
|
||||||
@@ -158,7 +159,7 @@ export function PyodideWebView() {
|
|||||||
return (
|
return (
|
||||||
<WebView
|
<WebView
|
||||||
ref={pyodideRef}
|
ref={pyodideRef}
|
||||||
source={{ html: PYODIDE_HTML, baseUrl: '' }}
|
source={{ html: PYODIDE_HTML, baseUrl: 'https://localhost' }}
|
||||||
style={styles.hidden}
|
style={styles.hidden}
|
||||||
onMessage={handleWebViewMessage}
|
onMessage={handleWebViewMessage}
|
||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
|
|||||||
@@ -54,10 +54,12 @@ export function handleWebViewMessage(e: WebViewMessageEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wheelBase64 is the bincio .whl file pre-fetched by the React Native side
|
||||||
|
// (native networking supports HTTP on local network; WKWebView does not).
|
||||||
export function extractFile(
|
export function extractFile(
|
||||||
filename: string,
|
filename: string,
|
||||||
base64: string,
|
base64: string,
|
||||||
wheelUrl: string,
|
wheelBase64: string,
|
||||||
onStatus: (msg: string) => void = () => {},
|
onStatus: (msg: string) => void = () => {},
|
||||||
): Promise<ExtractionResult> {
|
): Promise<ExtractionResult> {
|
||||||
if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress'));
|
if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress'));
|
||||||
@@ -67,7 +69,7 @@ export function extractFile(
|
|||||||
|
|
||||||
isExtracting = true;
|
isExtracting = true;
|
||||||
const reqId = String(++reqCounter);
|
const reqId = String(++reqCounter);
|
||||||
const args = JSON.stringify({ reqId, filename, base64, wheelUrl });
|
const args = JSON.stringify({ reqId, filename, base64, wheelBase64 });
|
||||||
|
|
||||||
return new Promise<ExtractionResult>((resolve, reject) => {
|
return new Promise<ExtractionResult>((resolve, reject) => {
|
||||||
pending.set(reqId, {
|
pending.set(reqId, {
|
||||||
|
|||||||
Reference in New Issue
Block a user