Skip to content
scalive
Menu
ConnectingLiveReconnectingOffline

Troubleshooting

Prerequisites

Start with a reproducible URL, server logs, and access to the browser console and Network panel. Identify whether the expected result belongs to the initial HTTP render or the connected LiveView; the checks below diagnose those phases separately.

Separate The Two Mounts

A LiveView mounts once for the disconnected HTTP response and again whenever its socket joins or rejoins. Start diagnosis by deciding which phase failed:

  1. Fetch the page with JavaScript disabled or inspect the document request. A non-200 response, missing document, or incorrect initial HTML is a disconnected startup or routing problem.

  2. If the document is correct, inspect the browser console and Network panel. The Phoenix client should load and open a WebSocket below the configured socket path.

  3. If the socket joins but an interaction fails, inspect the event binding, decoded message, handler failure, and returned diff rather than changing the HTTP route.

mount must be repeatable. The disconnected model is not continued by the connected socket, and a rejoin creates another connected model. Restrict subscriptions and other connection-only effects to the Connection.Connected(capabilities) branch of ctx.connection; persist state outside the LiveView when it must survive reconnects or process restarts.

Diagnose Startup Failures

Check the startup path in order:

  • Build the browser bundle before starting the JVM.

  • Load every declared asset with StaticAssets.load.

  • Validate one ZioHttpConfig and pass it to ZioHttp.routes.

  • Attach a complete root layout containing <html>, <head>, and <body>.

  • Combine the Live routes with assets.routes.

  • Provide all ZIO environment requirements before calling Server.serve.

StaticAssets.load fails startup when a configured classpath or directory asset is absent. Route construction also validates invalid or duplicate Live route configurations. Preserve those failures rather than replacing them with a server that starts partially.

For production, require one stable, high-entropy signing secret on every replica. ZioHttpConfig.apply rejects a secret shorter than 32 UTF-8 bytes and a zero or negative session age. Surface either result as a startup failure. A random process-local fallback makes restarts invalidate session, flash, mount-claim, and CSRF tokens, and prevents replicas from accepting one another's tokens.

Use secure cookies behind HTTPS:

scala
val config = ZioHttpConfig(
  signingSecret = requiredSecret,
  sessionMaxAge = java.time.Duration.ofDays(7),
  secureCookie = true
).fold(error => throw IllegalArgumentException(error.toString), identity)

val security = LiveSecurity(config)

The secureCookie value is explicit; local HTTP may use false, while browser-facing HTTPS should use true.

Diagnose Missing Assets

If the HTML renders but remains disconnected, first verify that the browser client bundle loaded successfully. Inspect the rendered digested URL and request it directly. A working setup uses the same StaticAssets value both to render the URL and to serve its routes:

scala
import zio.http.Server

for
  assets <- StaticAssets.load(
              StaticAssetConfig.classpath("public", Seq("app.js", "app.css"))
            )
  application = Live.router
                  .withRootLayout(RootLayout(assets))(
                    Routes.home -> HomeLiveView()
                  )
  liveRoutes = ZioHttp.routes(application, security)
  routes = liveRoutes ++ assets.routes
  _ <- Server.serve(routes).provide(Server.default)
yield ()

trackedScript and trackedStylesheet emit fingerprinted paths and phx-track-static. By default, digested assets are cached as immutable for one year, while configured original paths use no-cache. Do not hard-code a digest; render it through the loaded manifest. If a reverse proxy adds a URL prefix, make its routing agree with StaticAssetConfig.mountPath rather than rewriting only the HTML.

Common asset failures are a missing Mill resources dependency on the bundle, an output name omitted from bundleOutputs, a classpath prefix that does not match the packaged resource, or forgetting assets.routes. Return to the quick-start asset wiring for a minimal known shape.

Diagnose Socket Connections

The default browser configuration is new LiveSocket("/live", Socket, ...). The Phoenix client opens the WebSocket endpoint at /live/websocket. If the router uses another mount, configure both sides:

scala
import zio.http.codec.PathCodec

val liveRoutes = Live.router
  .withSocketPath(PathCodec.empty / "socket")(
    Routes.home -> HomeLiveView()
  )
js
const liveSocket = new LiveSocket("/socket", Socket, { params })

A reverse proxy must forward the WebSocket upgrade on the resulting /socket/websocket path. Check the actual request URL, upgrade response, proxy timeouts, and browser console before debugging event handlers.

Scalive currently implements WebSocket transport. Phoenix Channels long-poll fallback is not implemented, and endpoint options such as long-poll and hibernation are not available. A deployment that blocks WebSockets therefore cannot rely on transport fallback.

Diagnose CSRF Rejections

When ZioHttp.routes receives its validated config, the disconnected render injects a <meta name="csrf-token"> element into the root layout's <head> and issues the matching _scalive_csrf cookie. The browser must return the meta value as _csrf_token in LiveSocket params:

js
const csrfToken = document
  .querySelector("meta[name='csrf-token']")
  ?.getAttribute("content")
const params = csrfToken ? { _csrf_token: csrfToken } : {}
const liveSocket = new LiveSocket("/live", Socket, { params })

Check all parts of the pair:

  • The root layout renders a real <head> so token injection has a target.

  • The JavaScript reads the server-issued token rather than generating one.

  • The browser stores and sends _scalive_csrf to the WebSocket endpoint.

  • Cookie domain, path, Secure, and same-site behavior match the public origin.

  • Every replica uses the same SCALIVE_TOKEN_SECRET.

  • A proxy preserves the query string and cookie header on the upgrade request.

Missing, expired, transferred, or tampered token/cookie pairs are rejected. A failure at this point can appear as a stale join rather than an HTTP form error. Ordinary checked POST forms use the same protection and receive a hidden _csrf_token; do not remove that generated field.

Diagnose Reconnects And Crashes

On transport interruption, expect the Phoenix client to attempt reconnection and the server to mount a fresh connected lifecycle. Verify that:

  • mount does not assume the disconnected model or an earlier socket still exists;

  • connection-scoped streams and fibers are acquired through lifecycle capabilities so old resources can be released;

  • durable user state is loaded from a shared service rather than only from the previous model;

  • repeated mount side effects are idempotent or explicitly guarded;

  • deployed replicas share the signing secret; and

  • proxy idle timeouts are not repeatedly terminating healthy sockets.

The runtime emits a Phoenix-compatible phx_error when a joined root LiveView crashes, and native tests demonstrate that the topic can subsequently rejoin with a new socket. Do not turn that result into a stronger guarantee: exact protocol error payloads, stale cases, crash logging, and reconnect/remount parity still need systematic upstream auditing. Write a browser test for the concrete recovery behavior your application promises.

Tracked static assets and ConnectedMetadata.staticChanged are implemented, but explicit typed connect-info support remains partial. Avoid designing recovery logic around undocumented raw connect parameters.

Account For Current Limits

Current operational and diagnostic limits include:

  • no long-poll transport fallback;

  • connected server-side testing does not cross the real browser DOM boundary;

  • no complete telemetry or observability API, although selected runtime branches log warnings and errors;

  • partial typed connect-params and connect-info support;

  • expanding rather than exhaustive protocol error and reconnect parity; and

  • signed but not encrypted session claims and flash values.

These limits do not mean the corresponding core behavior is absent. They define where Scalive does not yet promise a complete public API or audited upstream matrix. Consult the compatibility matrix before relying on an edge case, and use the boundary guidance in Testing LiveViews to add evidence for your application's requirements.