You are reading the docs for v0.8. Read the latest (v0.10)
Troubleshooting
Each section is a symptom as you would see it: a status, a body, a log line or a panic. The quoted text is what via prints.
On this page
- Every action answers 403 forbidden origin
- The page never updates live
- Clicks answer 410 Gone
- A Disconnected banner, or the tab keeps reloading
- OnConnect or OnDispose never runs
- Mount panics at startup
- A page answers 500 render failed
- Rows keep the wrong state after a delete or reorder
- An upload answers 413 request body too large
- Connects or actions answer 503
- EvalError: Refused to evaluate a string as JavaScript
- Copy to clipboard does nothing
#Every action answers 403 forbidden origin
403 forbidden originvia.WithTrustedOrigin turned the origin check on and the browser's request fails it. The allowlist compares exact strings, so the usual misses are:
- A different spelling:
http://forhttps://,www., a port, a trailing slash. - A sibling subdomain: its
Sec-Fetch-Siteissame-site, which fails. List it. - A proxy that strips
Sec-Fetch-*or rewritesHostto the upstream address. nginx needsproxy_set_header Host $host;. - An empty origin read from the environment, like the
VIA_ORIGINthe Deploy page's main.go reads, so the listed value is not the site's.
func newRouter() *via.Router {
return via.NewRouter(
// Exactly as the browser sends it: scheme, host, port if not default.
via.WithTrustedOrigin("https://example.com"),
via.WithTrustedOrigin("https://app.example.com"),
)
}
A 403 with the body session mismatch is a different check: a live action carried a different session than the one its tab connected with. The cookie expired or was cleared, or another app on the same host overwrote it; give each app its own via.WithSessionCookieName. A bare forbidden is your OnInit or OnReload returning via.ErrForbidden.
See Sessions & security: Origin checks and CSRF.
#The page never updates live
Clicks POST and answer 200, or a Tick runs on the server, and the page does not change. Check in this order:
- No live unit. A page streams only if a unit renders a
via.Stateorvia.List, or registersvia.Ctx.Tickorvia.Ctx.ListeninOnInit. A page of Signals is plain: an action's response patches it, and nothing else can. - The proxy buffers the stream. Caddy needs
flush_interval -1on thereverse_proxy; nginx needsproxy_buffering off;. Compression middleware ontext/event-streamholds frames back the same way. - An idle timeout under 25 s. via writes a keepalive every 25 seconds, fixed. A proxy or balancer that closes a silent connection sooner drops every quiet stream; 60 s is enough.
- The State sits behind a closed branch. The GET rendered no State, so the page was served plain, and the action that opens the branch fails:
500 action failed
level=ERROR msg="via: action panic" err="via: this action made a unit live on a page that was served plain — the tab has no stream. Every action after this one would 410. Liveness must not depend on a branch an action can open: render the State unconditionally, or register a Tick/Listen in OnInit so the page is live from the first render"Render the State outside the via.When, or register the Tick or Listen in OnInit so the unit is live from the first render.
See Deploy.
#Clicks answer 410 Gone
410 no such action cGAmF8Xg; this render does not bind it
410 this render does not bind that action for that argument
410 no such child
410 this tab id has no open stream for this page: the connection closed, or the id belongs to a page that is gone — reload
410 this action needs the page's tab id and the request carried no viatab signal: the page never opened its stream, or the id was filtered outlevel=WARN msg="via: no such action; this render binds others" act=cGAmF8Xg binds="Of-LgGbQ (main.(*Todos).Delete-fm)"
level=WARN msg="via: the action is bound, but not for this arg" act=main.(*Todos).Delete-fm arg="\"2\"" bound=1An action is dispatchable only if the render before it bound that handler, with that argument, in that child. A 410 means the render the server last ran no longer contains what the client clicked: the button sits inside a via.When that is now closed, the on.WithArg argument changed since the page was drawn, or the via.Child moved. The two stream messages are a live unit's action with no open stream behind it.
- Bind a stable key as the argument, a row's primary key, never a position, count or cursor.
- Gate
via.Whenon session or database state, and keep any When around avia.Childfixed for the life of the page: it shifts later siblings' keys. - On a plain page, a handler that exists only inside a branch opened by a bound Signal always 410s: the render that authorises runs before the posted signals apply. Move the handler outside the branch.
The browser reloads the tab on its own after a 410, with a "Page is out of date - reloading…" banner.
See Actions: Arguments.
#A Disconnected banner, or the tab keeps reloading
via's reconnect script shows a banner at the top of the page. What each state means:
| Banner | Cause | What happens next |
|---|---|---|
| Reconnecting… | The network dropped the stream. | The browser retries; the banner clears on the next patch. |
| Disconnected. | The stream ended cleanly: a deploy, a restart, Router.Close. | Probes the page URL with backoff (0.5 s to 8 s) and reloads once it answers. |
| Page is out of date - reloading… | A request answered 410. | Probes, then reloads. |
| Disconnected., and it stays | A request answered 403 or 5xx; or two automatic reloads already; or 20 probes failed. | Nothing until you press Reconnect, which clears the reload count. |
Every banner but Reconnecting… carries a Reconnect button that probes at once.
A banner right after every deploy is expected; state that lives only in a via.State starts over, since a reconnect is a new connection. A Disconnected banner that stays on first load is a 403 or 5xx: read the Network tab for the status and find it on this page. The reload count clears 5 seconds after a load, so a server that fails straight after every reload stops at two.
See Live state: Connection lifecycle.
#OnConnect or OnDispose never runs
func (r *Room) OnInit(ctx *via.Ctx) error {
ctx.OnConnect(r.join)
ctx.OnDispose(r.leave)
return nil
}
func (r *Room) join() { online.Add(1) }
func (r *Room) leave() { online.Add(-1) }
Besides View and PageMeta, v0.8 calls two methods: OnInit and OnReload. A v0.7 OnConnect(ctx *via.Ctx) error or OnDispose(ctx *via.Ctx) method still compiles and is never called, and on a type that has OnInit via says nothing. Register both from OnInit with via.Ctx.OnConnect and via.Ctx.OnDispose. They run only on a unit something else made live; on a plain unit there is no connection to open or close.
A method named like a hook (Init, Oninit, Refresh, a v0.7 OnConnect) with the hook's exact signature, on a type that lacks the hook, warns once:
level=WARN msg="via: main.Room.Oninit looks like a mis-named OnInit — it has the hook's exact signature but main.Room has no OnInit func(*via.Ctx) error, so nothing will ever call it. Rename it to OnInit."See Migrating from v0.7.
#Mount panics at startup
panic: via: main.Room.OnInit has signature func(*via.Ctx), not func(*via.Ctx) error — so the hook will never run
panic: via: main.Search.View has a VALUE receiver and the composition holds Signals — View must take a POINTER receiver (func (p *Search) View() h.H), or every rendered Signal binds against a discarded copy
panic: via: Mount path "/files/{path...}": a {name...} wildcard is not supported — a page's action and stream routes live under its pathRegistration mistakes panic when the router is built, before it serves: a hook with the wrong signature, a View with a value receiver on a type holding Signals, a {name...} wildcard or a reserved path segment, a session key under 16 bytes, two mounts on one path. The message names the type or the pattern; fix that and restart.
See Compositions.
#A page answers 500 render failed
A composition mistake that only a render can see. The body is always render failed; the log line says which.
#A live unit inside another live unit
500 render failed
level=ERROR msg="via: render panic" err="via: via.Child: a live unit cannot sit inside another live unit — embed it directly from a plain ancestor instead"A live unit may not contain another live unit, and a live child's View may not call via.Child at all. Keep the live units as siblings under a plain parent:
type Dashboard struct {
Chat Chat
Clock Clock
Feed Feed
}
func (d *Dashboard) View() h.H {
return h.Main(via.Child(d.Chat), via.Child(d.Clock), via.Child(d.Feed))
}
#via.Child given a pointer
500 render failed
level=ERROR msg="via: render panic" err="via: via.Child(child) requires child to have a View() method"via.Child takes the child by value and calls View on its own copy, so via.Child(&p.Chat) hands it a **Chat. Pass the field: via.Child(p.Chat).
See Compositions.
#Rows keep the wrong state after a delete or reorder
func (l *Todos) row(t Todo) h.H {
return h.Li(h.ID("todo-"+strconv.Itoa(t.ID)),
h.Input(h.Type("checkbox")),
h.Str(t.Title),
h.Button(on.Click(on.WithArg(l.Delete, t.ID)), h.Str("delete")),
)
}
func (l *Todos) View() h.H { return h.Ul(via.Each(l.items, l.row)) }
Datastar morphs a re-rendered list by position. Delete the second of three rows without ids and the browser keeps the second element and rewrites its text, so a ticked checkbox, typed input or open <details> now belongs to the row below. A stable id per row makes the morph match by id. The click itself is not affected: on.WithArg carries the row's key.
See Actions: Arguments.
#An upload answers 413 request body too large
413 request body too largeTwo caps: via.WithMaxBody (1 MiB) for a JSON action body, and via.WithMaxUpload (8 MiB) for the whole multipart body of a via.PostForm submit. Raise the one that matches; a proxy in front may have its own limit, such as nginx's client_max_body_size.
func newUploadRouter() *via.Router {
return via.NewRouter(
via.WithMaxUpload(64<<20), // a PostForm submit's whole body
via.WithMaxBody(4<<20), // a JSON action body
)
}
See Sessions & security › Request limits.
#Connects or actions answer 503
503 stream capacity reached
level=WARN msg="via: refusing an SSE connect with 503: the router is at its live-stream cap. There is no per-IP share of that cap, so a single client can hold all of it; every tab is refused until one closes. The limit is WithMaxSSEConn — raise it only with the memory to back it." open=10000 cap=10000
503 stream busy
level=WARN msg="via: live action queue not drained — the connection's goroutine is blocked inside a Tick, Listen or action handler, so its keepalives have stopped too and every action on this tab answers 503 until it returns. Move blocking work off the handler."stream capacity reached is the router at via.WithMaxSSEConn (10,000 open streams). The cap is router-wide with no per-client share; raise it only with the memory for one goroutine and one composition tree per stream.
stream busy is one tab's goroutine stuck in a handler for longer than via.WithPinnedDeadline (5 s). Tick, Listen and actions on a tab run one at a time, so a blocking call in any of them stalls the rest. Do slow work in a goroutine and publish the result back through a topic. A 503 with session store unavailable is a via.SessionStore that failed to load.
See Sessions & security › Request limits.
#EvalError: Refused to evaluate a string as JavaScript
EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of scriptDatastar's bindings need 'unsafe-eval', and via's own policy has it. The error means a second policy reached the browser, from middleware or the proxy (nginx add_header, Caddy header), and browsers enforce every policy they receive. Remove the extra header, or add 'unsafe-eval' to its script-src. Asset origins belong in PageMeta().Assets, which via folds into its own policy.
See Sessions & security › Content-Security-Policy.
#Copy to clipboard does nothing
expr.CopyToClipboard and expr.CopyTextOf call navigator.clipboard.writeText and swallow a refused write, so it fails with nothing in the console. Browsers allow it in a secure context (HTTPS, or localhost), from a user gesture, in a focused document. Over plain HTTP on a LAN address navigator.clipboard is undefined and the call throws; from on.LoadCS or a background tab the write is refused. Bind it to on.ClickCS and serve over HTTPS.
See Signals: Expressions.