From e6aa28ca4cee7b936a0bee3da130d33d43fa8a44 Mon Sep 17 00:00:00 2001 From: tomdebone Date: Sat, 18 Jul 2026 18:53:35 +0200 Subject: [PATCH] fix(tauri-app): use absolute AppView URL for /api/profile fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Tauri webview's origin is the Vite dev server (port 1430), not the AppView (port 2584). A relative `fetch('/api/profile/…')` resolves against Vite, which has no proxy configured, so the request lands on Vite's 404 HTML page and `response.json()` then throws `SyntaxError: The string did not match the expected pattern.` The error surfaced as `err: SyntaxError…` under the banner of the redesigned ProfileView. Fix: expose the AppView base URL the Tauri shell was started with as a sync `get_api_urls` Tauri command. The Rust side reads the URL from `MAARCADETWEET_APPVIEW_URL` (default `http://127.0.0.1:2584`) at startup and stores it on `AppState` so the command doesn't need to re-read the env. The frontend exposes a cached `getAppviewUrl()` helper; ProfileView's `load()` uses it to build an absolute fetch URL. The relative-path bug also affected the previous UserProfileView, but it never errored loudly enough for the user to notice — the new X-style layout made the err block visible. --- crates/tauri-app/src-tauri/src/lib.rs | 32 +++++++++++++++++-- crates/tauri-app/src-tauri/src/state.rs | 5 +++ crates/tauri-app/src/lib/api/client.ts | 27 ++++++++++++++++ .../src/lib/components/ProfileView.svelte | 8 ++++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/crates/tauri-app/src-tauri/src/lib.rs b/crates/tauri-app/src-tauri/src/lib.rs index 0643205..5309827 100644 --- a/crates/tauri-app/src-tauri/src/lib.rs +++ b/crates/tauri-app/src-tauri/src/lib.rs @@ -543,8 +543,9 @@ pub fn run() { .unwrap_or_else(|_| "http://127.0.0.1:2584".to_string()); let state = AppState { - pds: PdsHttpClient::new(pds_url), - appview: AppViewClient::new(appview_url), + pds: PdsHttpClient::new(pds_url.clone()), + appview: AppViewClient::new(appview_url.clone()), + appview_url, store: store::SessionStore::new(), }; @@ -722,6 +723,7 @@ pub fn run() { open_external_url, profile_get_record, profile_set, + get_api_urls, ]) .run(tauri::generate_context!()) .expect("error while running maarcadetweet"); @@ -741,6 +743,32 @@ async fn profile_get_record( .map_err(|e| e.to_string()) } +/// Frontend-side base URLs the Tauri shell was started with. Used by +/// the Svelte components to build absolute fetch URLs — a relative +/// `/api/...` resolves against the Vite dev origin (port 1430), not +/// the AppView (port 2584), and the Vite server has no proxy +/// configured, so the fetch lands on a 404 HTML page and +/// `response.json()` throws `SyntaxError`. +#[derive(serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct ApiUrls { + pds_url: String, + appview_url: String, +} + +#[tauri::command] +fn get_api_urls(state: tauri::State<'_, AppState>) -> ApiUrls { + // Sync command — the URLs are immutable for the lifetime of the + // Tauri shell (read from MAARCADETWEET_*_URL at startup), so no + // async machinery is needed. Returns the AppView URL the + // frontend needs; PDS URL is exposed too so future fetch-based + // XRPC calls don't have to add their own command. + ApiUrls { + pds_url: state.pds.base_url.clone(), + appview_url: state.appview_url.clone(), + } +} + #[tauri::command] async fn profile_set( state: tauri::State<'_, AppState>, diff --git a/crates/tauri-app/src-tauri/src/state.rs b/crates/tauri-app/src-tauri/src/state.rs index 94b7bfc..aab1d63 100644 --- a/crates/tauri-app/src-tauri/src/state.rs +++ b/crates/tauri-app/src-tauri/src/state.rs @@ -1,5 +1,10 @@ pub struct AppState { pub pds: crate::pds_client::PdsHttpClient, pub appview: crate::appview_client::AppViewClient, + /// Base URL of the AppView service (`http://host:port`, no + /// trailing slash). Stored verbatim so the frontend can build + /// absolute URLs for fetch calls — a relative `/api/profile/…` + /// would resolve against the Vite dev origin, not the AppView. + pub appview_url: String, pub store: crate::store::SessionStore, } diff --git a/crates/tauri-app/src/lib/api/client.ts b/crates/tauri-app/src/lib/api/client.ts index 5fe25d9..e0310f1 100644 --- a/crates/tauri-app/src/lib/api/client.ts +++ b/crates/tauri-app/src/lib/api/client.ts @@ -24,6 +24,33 @@ async function tauriCall(cmd: string, fallback: T, args?: Record(cmd, args); } +/// Base URLs the Tauri shell was started with. Exposed via the +/// `get_api_urls` command so the Svelte components can build +/// absolute fetch URLs — a relative `/api/...` resolves against +/// the Vite dev origin (port 1430), not the AppView (port 2584), +/// and `response.json()` then throws `SyntaxError` on the 404 +/// HTML page. Cached after the first successful call. +let _apiUrlsCache: { pdsUrl: string; appviewUrl: string } | null = null; + +export type ApiUrls = { pdsUrl: string; appviewUrl: string }; + +/// Fetch the AppView + PDS base URLs from the Rust shell. Returns +/// the cached value on subsequent calls. +export async function getApiUrls(): Promise { + if (_apiUrlsCache) return _apiUrlsCache; + const urls = await safeInvoke("get_api_urls"); + _apiUrlsCache = urls; + return urls; +} + +/// Convenience: just the AppView base URL (the only one the UI +/// currently needs for direct fetch calls). Same caching as +/// `getApiUrls`. +export async function getAppviewUrl(): Promise { + const { appviewUrl } = await getApiUrls(); + return appviewUrl; +} + /** * Strict variant of `tauriCall` for actions that MUST hit the * Tauri runtime (login, register, logout, post, like, etc.). In diff --git a/crates/tauri-app/src/lib/components/ProfileView.svelte b/crates/tauri-app/src/lib/components/ProfileView.svelte index fa5db70..7f58c05 100644 --- a/crates/tauri-app/src/lib/components/ProfileView.svelte +++ b/crates/tauri-app/src/lib/components/ProfileView.svelte @@ -6,6 +6,7 @@ pickAndUploadImage, fetchBlob, releaseBlob, + getAppviewUrl, } from "../api/client"; import { onDestroy, onMount } from "svelte"; @@ -74,7 +75,12 @@ async function load() { viewModel = { kind: "loading" }; try { - const r = await fetch(`/api/profile/${encodeURIComponent(handle)}`); + // Absolute URL because the Tauri webview's origin is the Vite + // dev server (port 1430), not the AppView (port 2584) — a + // relative `/api/profile/…` would resolve against Vite, hit a + // 404 HTML page, and `r.json()` would throw `SyntaxError`. + const base = await getAppviewUrl(); + const r = await fetch(`${base}/api/profile/${encodeURIComponent(handle)}`); if (!r.ok) { viewModel = { kind: "error",