From c9c5caf3ff976800dcead60aec6f1bfe6ef8dca9 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Sat, 15 Aug 2026 12:18:15 +0200 Subject: [PATCH] Stable server fn endpoints: open tabs survive deploys The 'previous question's resources until refresh' mystery: leptos's auto-generated server fn routes embed a hash that changes across builds. Every deploy therefore breaks every already-open tab - its wasm keeps calling /api/get_question, the new server answers 400 'Could not find a server function at the route', and client-side navigation quietly leaves the previous question's data on screen. Refresh loads the new wasm with matching hashes, which is why it always fixed it. Confirmed live: a pre-deploy client 400ed on get_question2970801986613442004 while the freshly served wasm calls get_question13103328088426240960, same source on both builds. Explicit endpoint names decouple the URL from the build. This deploy is the last breaking one; after it, old clients keep working. Co-Authored-By: Claude Fable 5 --- .claude/settings.local.json.tmp.280892.a95c86d97b21 | 10 ++++++++++ src/answers.rs | 4 ++-- src/app.rs | 6 +++--- src/auth.rs | 2 +- src/resource.rs | 6 +++--- 5 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 .claude/settings.local.json.tmp.280892.a95c86d97b21 diff --git a/.claude/settings.local.json.tmp.280892.a95c86d97b21 b/.claude/settings.local.json.tmp.280892.a95c86d97b21 new file mode 100644 index 0000000..19d6e13 --- /dev/null +++ b/.claude/settings.local.json.tmp.280892.a95c86d97b21 @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo check *)", + "Bash(git add *)", + "Bash(git commit -m 'Stable server fn endpoints: open tabs survive deploys *)", + "Bash(git push *)" + ] + } +} diff --git a/src/answers.rs b/src/answers.rs index ee8800f..8a90c0e 100644 --- a/src/answers.rs +++ b/src/answers.rs @@ -101,7 +101,7 @@ pub struct TransitionItem { /// (the shared per-alternative Confirm button). Items are independent /// - per-item CAS, no cross-item transaction - so one failure doesn't /// roll back the others; failures are collected and reported together. -#[server] +#[server(endpoint = "transition_answers")] pub async fn transition_answers( question_id: String, alternative: String, @@ -289,7 +289,7 @@ async fn apply_transition( /// itself. Deliberately returns the same generic error for "no such /// item" and "email doesn't match", so an unsubscribe link can't be /// used to probe which hashes or emails exist. -#[server] +#[server(endpoint = "self_transition_answer")] pub async fn self_transition_answer( question_id: String, alternative: String, diff --git a/src/app.rs b/src/app.rs index 5755079..51ee6c9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1484,7 +1484,7 @@ fn NotFound() -> impl IntoView { } } -#[server] +#[server(endpoint = "get_question")] pub async fn get_question(path: String) -> Result, ServerFnError> { use crate::server::AppState; let state = expect_context::(); @@ -1495,7 +1495,7 @@ pub async fn get_question(path: String) -> Result, ServerFnErro /// the site nav's data. Context-dependent: an owner session sees the /// gated pages too, and `followup` pages only appear once the visitor /// carries an answer chain. -#[server] +#[server(endpoint = "list_qualifying_questions")] pub async fn list_qualifying_questions( has_chain: bool, ) -> Result, ServerFnError> { @@ -1528,7 +1528,7 @@ pub struct SubmitResult { pub chain_hash: String, } -#[server] +#[server(endpoint = "submit_answer")] pub async fn submit_answer( question_id: String, alternative: String, diff --git a/src/auth.rs b/src/auth.rs index 2811336..b6b722e 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -19,7 +19,7 @@ pub struct User { pub const SESSION_USER_KEY: &str = "user"; /// Returns the currently signed-in user, if any. -#[server] +#[server(endpoint = "current_user")] pub async fn current_user() -> Result, ServerFnError> { let session: tower_sessions::Session = leptos_axum::extract().await?; let user = session diff --git a/src/resource.rs b/src/resource.rs index 485cf18..4eb14d4 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -16,7 +16,7 @@ use leptos::prelude::*; /// fetch (`GiteaStarred`/`GiteaOrgRepos`/`Url`), the mechanism behind /// "a resource parameterized by other form fields"; a `Kv` resource /// ignores them entirely, same as today. -#[server] +#[server(endpoint = "get_resource")] pub async fn get_resource( question_id: String, alternative: String, @@ -49,7 +49,7 @@ pub async fn get_resource( /// of whether it's displayed read-only or offered as choices to pick /// from, so this deliberately doesn't duplicate the source-dispatch or /// jq-shaping logic - see `fetch_resource_value`. -#[server] +#[server(endpoint = "get_requirement_options")] pub async fn get_requirement_options( question_id: String, alternative: String, @@ -80,7 +80,7 @@ pub async fn get_requirement_options( /// `get_requirement_options`: fetches the resource a bound field /// loads its value from, parameterized by the watched sibling's value /// (already inside `params`, keyed by the bind's param name). -#[server] +#[server(endpoint = "get_requirement_binding")] pub async fn get_requirement_binding( question_id: String, alternative: String,