SailfishOS build shows blank white screen — Expo Router cannot run from file:// URLs #6

Open
opened 2026-09-24 09:39:45 +02:00 by hermes · 2 comments
Collaborator

Problem

The published SailfishOS build (RPM harbour-orca, e.g. version 0.0.2014) opens a blank white window on the device. Reproduced and root-caused on a real SailfishOS phone (Gecko 115 WebView via sailfish-qml), and confirmed with jsdom reproductions of the shipped bundle.

Root cause

mobile/sailfish/qml/harbour-orca.qml loads the web bundle via a file:// URL:

url: Qt.resolvedUrl("../www/index.html?platform=sailfish")

which resolves to file:///usr/share/harbour-orca/www/index.html?platform=sailfish.

The bundle is an Expo Router web export. On startup Expo Router calls getInitialURL() (returns window.location.href) and computes the initial route from the URL pathname (extractExpoPathFromURL → getStateFromPath).

  1. The pathname is not a route. On a file:// URL the pathname is /usr/share/harbour-orca/www/index.html — an absolute filesystem path that matches no route in the app. Expo Router therefore renders its built-in Unmatched placeholder: <div class="expo-router-unmatched"> — visually a blank white screen. (Verified: loading the shipped 12 MB bundle over file:// in jsdom renders exactly this placeholder with no app UI; the ?platform=sailfish query does not participate in route matching.)
  2. Client-side URL fixing is impossible on file://. The obvious fix — window.history.replaceState(null, '', '/') in index.html — throws a SecurityError in Gecko (Firefox ESR 115, which is what SailfishOS WebViews are): history navigation is blocked on file:// URLs, for both replaceState and pushState. Verified on-device via sailfish-qml -e.
  3. The exact same, unmodified bundle works perfectly over HTTP at a clean path. Served with a trivial local static server at http://127.0.0.1:PORT/?platform=sailfish (e.g. python3 -m http.server on /usr/share/harbour-orca/www), the router resolves the / path, the initial route matches, and the full Orca UI renders (verified via jsdom: ~8 KB of real UI: "Orca", "Connect your desktop", "Pair Desktop"…). The platform=sailfish query parameter is preserved over HTTP and still selects the Sailfish code paths (isSailfishWeb()), so HTTP is not a downgrade versus file://.

Evidence matrix (jsdom, pristine 12 MB bundle, no patches)

URL Result
file:///usr/share/harbour-orca/www/index.html?platform=sailfish Unmatched placeholder (blank)
http://127.0.0.1:8484/index.html?platform=sailfish Unmatched placeholder (blank) — /index.html is not a route
http://127.0.0.1:8484/?platform=sailfish Full app UI renders

Secondary issue found

The bundle uses Array.prototype.toReversed() in module 3272 (the diagnoseConnection code path). toReversed exists only from Firefox 118 / V8 105+; Gecko 115 (SailfishOS) will throw TypeError: ... toReversed is not a function when the connection-diagnostics flow runs. Worth polyfilling or gating.

The bundle is fine; the problem is entirely in the SFOS wrapper/packaging. Two viable options:

Option A (minimal, no re-architecture): local HTTP server launched by the app.

  • Ship a small launcher script (e.g. /usr/bin/harbour-orca-launch, POSIX sh):
    • pick a free loopback port (8484–8488),
    • start python3 -m http.server <port> --bind 127.0.0.1 in /usr/share/harbour-orca/www (daemonized with setsid, log to ~/.local/share/),
    • open http://127.0.0.1:<port>/?platform=sailfish in sailfish-browser (or better, launch sailfish-qml harbour-orca and point the WebView at that URL).
  • RPM .spec: install the script in %install, keep everything else identical (still noarch).

Option B (keeps the native window): QML wrapper starts the server itself.

  • Add a QProcess (or a tiny C helper compiled in %build) to harbour-orca.qml that starts the same local HTTP server on startup and set the WebView url to http://127.0.0.1:<port>/?platform=sailfish; kill the process in Component.destruction. This keeps the app inside the Sailfish window (better integration: notifications, camera/getUserMedia, single app lifecycle) at the cost of the "no compiled code / noarch" simplification.

A one-line temporary workaround that already works (until this is fixed) is to serve the installed www/ directory over local HTTP and open http://127.0.0.1:<port>/?platform=sailfish in the browser.

Long-term: consider a native/QML phone client, or upstreaming file:// support to Expo Router.

## Problem The published SailfishOS build (RPM `harbour-orca`, e.g. version 0.0.2014) opens a **blank white window** on the device. Reproduced and root-caused on a real SailfishOS phone (Gecko 115 WebView via `sailfish-qml`), and confirmed with jsdom reproductions of the shipped bundle. ## Root cause `mobile/sailfish/qml/harbour-orca.qml` loads the web bundle via a `file://` URL: ```qml url: Qt.resolvedUrl("../www/index.html?platform=sailfish") ``` which resolves to `file:///usr/share/harbour-orca/www/index.html?platform=sailfish`. The bundle is an **Expo Router** web export. On startup Expo Router calls `getInitialURL()` (returns `window.location.href`) and computes the initial route from the URL **pathname** (`extractExpoPathFromURL` → `getStateFromPath`). 1. **The pathname is not a route.** On a `file://` URL the pathname is `/usr/share/harbour-orca/www/index.html` — an absolute filesystem path that matches no route in the app. Expo Router therefore renders its built-in `Unmatched` placeholder: `<div class="expo-router-unmatched">` — visually a blank white screen. (Verified: loading the shipped 12 MB bundle over `file://` in jsdom renders exactly this placeholder with no app UI; the `?platform=sailfish` query does not participate in route matching.) 2. **Client-side URL fixing is impossible on `file://`.** The obvious fix — `window.history.replaceState(null, '', '/')` in `index.html` — throws a `SecurityError` in Gecko (Firefox ESR 115, which is what SailfishOS WebViews are): history navigation is blocked on `file://` URLs, for both `replaceState` and `pushState`. Verified on-device via `sailfish-qml -e`. 3. **The exact same, unmodified bundle works perfectly over HTTP at a clean path.** Served with a trivial local static server at `http://127.0.0.1:PORT/?platform=sailfish` (e.g. `python3 -m http.server` on `/usr/share/harbour-orca/www`), the router resolves the `/` path, the initial route matches, and the full Orca UI renders (verified via jsdom: ~8 KB of real UI: "Orca", "Connect your desktop", "Pair Desktop"…). The `platform=sailfish` query parameter is preserved over HTTP and still selects the Sailfish code paths (`isSailfishWeb()`), so HTTP is not a downgrade versus `file://`. ### Evidence matrix (jsdom, pristine 12 MB bundle, no patches) | URL | Result | |---|---| | `file:///usr/share/harbour-orca/www/index.html?platform=sailfish` | Unmatched placeholder (blank) | | `http://127.0.0.1:8484/index.html?platform=sailfish` | Unmatched placeholder (blank) — `/index.html` is not a route | | `http://127.0.0.1:8484/?platform=sailfish` | **Full app UI renders** | ### Secondary issue found The bundle uses `Array.prototype.toReversed()` in module 3272 (the `diagnoseConnection` code path). `toReversed` exists only from Firefox 118 / V8 105+; Gecko 115 (SailfishOS) will throw `TypeError: ... toReversed is not a function` when the connection-diagnostics flow runs. Worth polyfilling or gating. ## Recommended fix The bundle is fine; the problem is entirely in the SFOS wrapper/packaging. Two viable options: **Option A (minimal, no re-architecture): local HTTP server launched by the app.** - Ship a small launcher script (e.g. `/usr/bin/harbour-orca-launch`, POSIX sh): - pick a free loopback port (8484–8488), - start `python3 -m http.server <port> --bind 127.0.0.1` in `/usr/share/harbour-orca/www` (daemonized with `setsid`, log to `~/.local/share/`), - open `http://127.0.0.1:<port>/?platform=sailfish` in `sailfish-browser` (or better, launch `sailfish-qml harbour-orca` and point the WebView at that URL). - RPM `.spec`: install the script in `%install`, keep everything else identical (still noarch). **Option B (keeps the native window): QML wrapper starts the server itself.** - Add a QProcess (or a tiny C helper compiled in `%build`) to `harbour-orca.qml` that starts the same local HTTP server on startup and set the WebView `url` to `http://127.0.0.1:<port>/?platform=sailfish`; kill the process in `Component.destruction`. This keeps the app inside the Sailfish window (better integration: notifications, camera/`getUserMedia`, single app lifecycle) at the cost of the "no compiled code / noarch" simplification. A one-line temporary workaround that already works (until this is fixed) is to serve the installed `www/` directory over local HTTP and open `http://127.0.0.1:<port>/?platform=sailfish` in the browser. Long-term: consider a native/QML phone client, or upstreaming `file://` support to Expo Router.
Collaborator

Fix in progress on fork_main (uncommitted): native-window loopback approach. harbour-orca-launch starts python3 -m http.server on fixed 127.0.0.1:8484 then execs sailfish-qml; QML points at http://127.0.0.1:8484/?platform=sailfish, external links still escape to Browser. sailfish:bundle now inlines a Gecko-115 toReversed() shim as the first <head> script. Verified locally: real staged bundle serves 200 at /?platform=sailfish with shim before bundle code, noarch RPM builds with /usr/bin/harbour-orca-launch (755) + python3-base Requires, mobile suite 4677 green. Caught one packaging bug already: nixpkgs rpm points _bindir at the store, fixed in CI with --define _bindir /usr/bin. Still needs: push + CI sailfish job green + on-device proof.

Fix in progress on fork_main (uncommitted): native-window loopback approach. harbour-orca-launch starts python3 -m http.server on fixed 127.0.0.1:8484 then execs sailfish-qml; QML points at http://127.0.0.1:8484/?platform=sailfish, external links still escape to Browser. sailfish:bundle now inlines a Gecko-115 toReversed() shim as the first <head> script. Verified locally: real staged bundle serves 200 at /?platform=sailfish with shim before bundle code, noarch RPM builds with /usr/bin/harbour-orca-launch (755) + python3-base Requires, mobile suite 4677 green. Caught one packaging bug already: nixpkgs rpm points _bindir at the store, fixed in CI with --define _bindir /usr/bin. Still needs: push + CI sailfish job green + on-device proof.
Collaborator

CI confirms the fix path: run #6286 mobile-sailfish job success, RPM harbour-orca-0.0.2015-1.noarch.rpm built with /usr/bin/harbour-orca-launch (755) in BUILDROOT. Remaining step is on-device proof: install, launch, confirm the Orca UI renders from the loopback URL.

CI confirms the fix path: run #6286 mobile-sailfish job success, RPM harbour-orca-0.0.2015-1.noarch.rpm built with /usr/bin/harbour-orca-launch (755) in BUILDROOT. Remaining step is on-device proof: install, launch, confirm the Orca UI renders from the loopback URL.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
donach/orca#6
No description provided.