{"record":{"id":"529862ead7a8b49b","repo":"BloopAI/vibe-kanban","slug":"client-preview-proxy-port-already-set-529862","errorCode":null,"errorMessage":"client preview proxy port already set","messagePattern":"client preview proxy port already set","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/server/src/startup.rs","lineNumber":51,"sourceCode":"    /// the Tauri desktop app — IP address literals aren't reliably\n    /// matched by ATS, which causes WebSocket connections to fail.\n    pub fn url(&self) -> String {\n        format!(\"http://localhost:{}\", self.port)\n    }\n\n    /// Run both the main and proxy servers until the shutdown token is cancelled.\n    pub async fn serve(self) -> anyhow::Result<()> {\n        // Start relay tunnel so the host registers with the relay server.\n        // This must happen after the port is known (it's needed for local\n        // proxying) and is shared between the standalone binary and Tauri.\n        self.deployment\n            .client_info()\n            .set_server_addr(self.main_listener.local_addr()?)\n            .expect(\"client server address already set\");\n        self.deployment\n            .client_info()\n            .set_preview_proxy_port(self.proxy_port)\n            .expect(\"client preview proxy port already set\");\n        relay_registration::spawn_relay(&self.deployment).await;\n\n        let app_router = routes::router(self.deployment.clone());\n        let proxy_router: axum::Router = routes::preview::subdomain_router(self.deployment.clone())\n            .layer(ValidateRequestHeaderLayer::custom(validate_origin));\n\n        let main_shutdown = self.shutdown_token.clone();\n        let proxy_shutdown = self.shutdown_token.clone();\n\n        let main_server = axum::serve(self.main_listener, app_router)\n            .with_graceful_shutdown(async move { main_shutdown.cancelled().await });\n        let proxy_server = axum::serve(self.proxy_listener, proxy_router)\n            .with_graceful_shutdown(async move { proxy_shutdown.cancelled().await });\n\n        let main_handle = tokio::spawn(async move {\n            if let Err(e) = main_server.await {\n                tracing::error!(\"Main server error: {}\", e);\n            }","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/crates/server/src/startup.rs#L33-L69","documentation":"Same one-shot setter pattern as the server address: `set_preview_proxy_port` can only be called once on the deployment's ClientInfo, and the `.expect(\"client preview proxy port already set\")` panics when the port was previously stored. It sits immediately after `set_server_addr` in `ServerHandle::serve()`, so any path that calls serve() twice (or sets the port elsewhere first) triggers it.","triggerScenarios":"A second invocation of `ServerHandle::serve()` on a deployment whose preview proxy port was already set — e.g. two ServerHandles wrapping one DeploymentImpl, or external code calling `client_info().set_preview_proxy_port()` before serve().","commonSituations":"Restart logic in the Tauri/desktop wrapper that reuses a deployment across server restarts; test code setting the proxy port manually then calling serve(); concurrent serve() invocations racing on the same ClientInfo.","solutions":["Always obtain a fresh deployment (fresh ClientInfo) for each server start instead of reusing the previous one","Ensure set_preview_proxy_port is called only from ServerHandle::serve and never elsewhere","Make the setter idempotent or add a guard so re-setting the same value is a no-op instead of a panic","Serialize startup so only one serve() call can run per process (OnceLock or a dedicated supervisor task)"],"exampleFix":"// before\nself.deployment.client_info().set_preview_proxy_port(self.proxy_port)\n    .expect(\"client preview proxy port already set\");\n// after\nif let Err(e) = self.deployment.client_info().set_preview_proxy_port(self.proxy_port) {\n    tracing::warn!(\"preview proxy port already set ({}), continuing\", e);\n}","handlingStrategy":"validation","validationCode":"// Guard against double serve()/port set:\nstatic SERVED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);\nfn should_serve() -> bool {\n    !SERVED.swap(true, std::sync::atomic::Ordering::SeqCst)\n}","typeGuard":null,"tryCatchPattern":"// Catch the panic from serve() when embedding:\nlet result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    // spawn serve() and drive it\n}));\nif result.is_err() { eprintln!(\"startup panicked (port already set?) — rebuild deployment\"); }","preventionTips":["Make set_preview_proxy_port the sole responsibility of ServerHandle::serve","Restart servers only with a newly initialized deployment","Avoid constructing multiple ServerHandles around one DeploymentImpl","Consider making the setter idempotent for same-value re-sets"],"tags":["startup","panic","one-shot-setter","proxy-port"],"backgroundTag":"one-shot-value-already-set","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}