Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.

fix(tauri): close webview startup race + harden dmg-smoke - #771

Merged
shiba4life merged 1 commit into
mainfrom
fix/tauri-webview-startup-race
Apr 30, 2026
Merged

fix(tauri): close webview startup race + harden dmg-smoke#771
shiba4life merged 1 commit into
mainfrom
fix/tauri-webview-startup-race

Conversation

@shiba4life

Copy link
Copy Markdown
Collaborator

Summary

Fixes the long-standing blank-window-on-launch bug ("this keeps happening"). Two changes:

1. Close the race in src-tauri/src/lib.rs (the actual fix)

The race chain:

src/server/embedded.rs::start_embedded_server_lazy()
  ├─ tokio::spawn(async move { server.run().await })   ← runner queued
  └─ Ok(handle)                                          ← returned NOW

lib.rs::run()
  ├─ rx.recv() → Ok(handle)                              ← unblocks
  ├─ tauri::Builder::default()...setup(|app| { … })
  ├─ WebviewWindowBuilder::new(URL)                      ← navigates
  └─ webview hits ECONNREFUSED on GET /                  ← blank

start_embedded_server_lazy sends Ok the instant the spawn happens — actix_web::HttpServer::bind is still pending in the spawned task. The webview navigates a few ms later and races. WebKit doesn't auto-retry on ECONNREFUSED — the error page sticks even after the backend comes up 3-5s later.

Fix: in setup(), after we have the channel Ok(handle), poll TcpStream::connect_timeout against 127.0.0.1:{server_port} for up to 15s before building the window. A successful TCP handshake means actix's accept loop is processing — by the time the webview opens its connection, the listener wins.

~25 lines of std::net::TcpStream — no new deps. Logs whether the readiness check succeeded so we can spot regressions in user logs.

2. Harden tauri-release.yml dmg-smoke

Existing smoke only probes /api/health (JSON), so it never exercised the webview-loadable path. Adds two assertions:

  • GET / must return the real React shell (presence of <script type=\"module\">). Catches the class of regression where build.rs's stub index.html got embedded instead of a real npm run build output — the stub passes /api/health (backend works) but opens the webview to a blank page.
  • The JS bundle the shell references must return 200 OK. Confirms RustEmbed wiring end-to-end.

Race fix is deterministic; smoke catches structural drift. Together they should make blank windows non-recurring.

Why dmg-smoke didn't catch this before

dmg-smoke launches the binary directly (not via open -a), and probes the embedded backend's /api/health. It never created a Tauri webview, so it couldn't see the race. The new GET / + JS-bundle checks add the next-cheapest layer of defense without needing a real webview.

Test plan

  • CI passes on this PR (Rust Fast under GH_PAT; clippy on the new TCP-poll code).
  • After merge, tag v0.3.15 (git tag -a v0.3.15 -m v0.3.15 main && git push origin v0.3.15).
  • tauri-release.yml's dmg-smoke runs the new GET / + JS-bundle checks against v0.3.15.
  • Tom replaces /Applications/FoldDB.app with v0.3.15 and the webview loads on first launch (no Cmd-R needed). If still blank, the eprintln log line "[FoldDB] Embedded server did not start accepting … within 15s" appears in Console.app — that'd mean a different / additional cause.

🤖 Generated with Claude Code

Closes the long-standing "blank window on launch" bug. Root cause:

  src/server/embedded.rs::start_embedded_server_lazy
    -> tokio::spawn(server.run())   // returns immediately
    -> Ok(handle)                    // BEFORE actix_web::bind()

So `start_fold_server` in lib.rs sends Ok(handle) on the channel the
moment the runner is *queued* — the listener hasn't bound yet. When the
window builder navigates to http://localhost:{port} a few ms later,
WebKit hits ECONNREFUSED, pins the error page, and never auto-retries.
Symptom: blank window, console says "Failed to load resource. Could not
connect to the server." even though the backend comes up ~3-5s later.

Fix: in src-tauri/src/lib.rs setup(), after we know the server thread
sent Ok(handle), poll TCP connect against 127.0.0.1:{server_port} for
up to 15s before WebviewWindowBuilder. A successful kernel-level
handshake means actix's accept loop is running too. ~25 lines of
std-only code (no new deps).

Also harden tauri-release.yml's dmg-smoke beyond /api/health:

  - Assert `GET /` returns the real React shell (has
    `<script type="module">`) — catches the class of bug where
    build.rs's stub index.html got embedded instead of a real
    `npm run build` output. The stub backend would still pass
    /api/health (JSON works fine) but the webview opens blank.
  - Actually fetch the JS bundle the shell references and assert
    200 OK — confirms RustEmbed wiring end-to-end.

Together: race fix is deterministic, smoke catches structural drift.
"This keeps happening" should stop happening.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shiba4life
shiba4life enabled auto-merge April 30, 2026 22:20
@shiba4life
shiba4life added this pull request to the merge queue Apr 30, 2026
Merged via the queue into main with commit d1d395f Apr 30, 2026
14 checks passed
@shiba4life
shiba4life deleted the fix/tauri-webview-startup-race branch April 30, 2026 22:47
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant