fix(tauri-app): use absolute AppView URL for /api/profile fetch

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.
This commit is contained in:
tomdebone
2026-07-18 18:53:35 +02:00
parent e4bcfbfa83
commit e6aa28ca4c
4 changed files with 69 additions and 3 deletions
+30 -2
View File
@@ -543,8 +543,9 @@ pub fn run() {
.unwrap_or_else(|_| "http://127.0.0.1:2584".to_string()); .unwrap_or_else(|_| "http://127.0.0.1:2584".to_string());
let state = AppState { let state = AppState {
pds: PdsHttpClient::new(pds_url), pds: PdsHttpClient::new(pds_url.clone()),
appview: AppViewClient::new(appview_url), appview: AppViewClient::new(appview_url.clone()),
appview_url,
store: store::SessionStore::new(), store: store::SessionStore::new(),
}; };
@@ -722,6 +723,7 @@ pub fn run() {
open_external_url, open_external_url,
profile_get_record, profile_get_record,
profile_set, profile_set,
get_api_urls,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running maarcadetweet"); .expect("error while running maarcadetweet");
@@ -741,6 +743,32 @@ async fn profile_get_record(
.map_err(|e| e.to_string()) .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] #[tauri::command]
async fn profile_set( async fn profile_set(
state: tauri::State<'_, AppState>, state: tauri::State<'_, AppState>,
+5
View File
@@ -1,5 +1,10 @@
pub struct AppState { pub struct AppState {
pub pds: crate::pds_client::PdsHttpClient, pub pds: crate::pds_client::PdsHttpClient,
pub appview: crate::appview_client::AppViewClient, 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, pub store: crate::store::SessionStore,
} }
+27
View File
@@ -24,6 +24,33 @@ async function tauriCall<T>(cmd: string, fallback: T, args?: Record<string, unkn
return invoke<T>(cmd, args); return invoke<T>(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<ApiUrls> {
if (_apiUrlsCache) return _apiUrlsCache;
const urls = await safeInvoke<ApiUrls>("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<string> {
const { appviewUrl } = await getApiUrls();
return appviewUrl;
}
/** /**
* Strict variant of `tauriCall` for actions that MUST hit the * Strict variant of `tauriCall` for actions that MUST hit the
* Tauri runtime (login, register, logout, post, like, etc.). In * Tauri runtime (login, register, logout, post, like, etc.). In
@@ -6,6 +6,7 @@
pickAndUploadImage, pickAndUploadImage,
fetchBlob, fetchBlob,
releaseBlob, releaseBlob,
getAppviewUrl,
} from "../api/client"; } from "../api/client";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
@@ -74,7 +75,12 @@
async function load() { async function load() {
viewModel = { kind: "loading" }; viewModel = { kind: "loading" };
try { 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) { if (!r.ok) {
viewModel = { viewModel = {
kind: "error", kind: "error",