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 LiveViewtrait LiveView[Msg, Model]Defines a server-rendered view with typed messages and model state. mounts once for the disconnected HTTP
response and again whenever its socket joins or rejoins. Start diagnosis by
deciding which phase failed:
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.
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.
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.
mountdef mount(ctx: MountContext): zio.package.Task[Model] 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.connectiondef connection: Connection[Connected]; persist
state outside the LiveViewtrait LiveView[Msg, Model]Defines a server-rendered view with typed messages and model state. 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.loaddef load(config: StaticAssetConfig): zio.package.Task[StaticAssets]Loads and validates an asset manifest. Bytes are read again when a request is served..Validate one
ZioHttpConfigclass ZioHttpConfigValidated security configuration for the ZIO HTTP transport. and pass it toZioHttp.routesdef routes[R](application: LiveApplication[R], config: ZioHttpConfig): zio.http.Routes[R, Nothing].Attach a complete root layout containing
<html>,<head>, and<body>.Combine the Live routes with
assets.routesval routes: zio.http.Routes[Any, Nothing]GET and HEAD routes serving manifest entries below the configured mount path..Provide all ZIO environment requirements before calling
Server.serve.
StaticAssets.loaddef load(config: StaticAssetConfig): zio.package.Task[StaticAssets]Loads and validates an asset manifest. Bytes are read again when a request is served. 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.applydef apply(signingSecret: String, sessionMaxAge: java.time.Duration, secureCookie: Boolean): Either[ZioHttpConfig.Error, ZioHttpConfig] 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:
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 StaticAssetsobject StaticAssets
value both to render the URL and to serve its routes:
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 ()trackedScriptdef trackedScript[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg]Renders a tracked <script> using the asset's digested URL. and
trackedStylesheetdef trackedStylesheet[Msg](asset: String, mods: Mod[Msg]*): HtmlElement[Msg]Renders a tracked stylesheet <link> using the asset's digested URL. 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.mountPathval mountPath: zio.http.Path 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.routesval routes: zio.http.Routes[Any, Nothing]GET and HEAD routes serving manifest entries below the configured mount path.. 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:
import zio.http.codec.PathCodec
val liveRoutes = Live.router
.withSocketPath(PathCodec.empty / "socket")(
Routes.home -> HomeLiveView()
)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:
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_csrfto 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:
mountdef mount(ctx: MountContext): zio.package.Task[Model] 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
LiveViewtrait LiveView[Msg, Model]Defines a server-rendered view with typed messages and model state.
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.
Related Tasks
Recheck browser and asset wiring in Client setup and static assets.
Add evidence at the failing boundary with Testing LiveViews.
Resolve missing startup dependencies with Services and dependency injection.