Skip to content

fix(render): recover View panics in initial render and re-render (#48)#50

Merged
joaomdsg merged 2 commits into
mainfrom
fix/view-recover-48
May 28, 2026
Merged

fix(render): recover View panics in initial render and re-render (#48)#50
joaomdsg merged 2 commits into
mainfrom
fix/view-recover-48

Conversation

@joaomdsg

Copy link
Copy Markdown
Member

Closes #48.

Problem

View renders had no panic recovery, unlike action handlers (WithActionErrorHandler) and the OnInit/OnConnect/OnDispose lifecycle guards. A panic in viewFn escaped the framework:

  • Initial GET render — propagated to the embedding http.Server: naked stderr stack + dropped connection (no controlled 500, no via log line).
  • Async re-render (flushDirty) — escaped via two callers: the action autoflush defer (dropped the action POST connection) and the go c.SyncNow() broadcast goroutine, where an unrecovered panic crashed the whole process.

The asymmetry was the surprise: the same programmer mistake got wildly different treatment depending on which callback it sat in — and the precise on.* panic messages from #36 surface at View time, exactly where the framework didn't structure them.

Fix

Two sibling guards wrap every viewFn callsite, matching the existing recovery patterns:

  • renderView (initial GET) — recovers, logs via a.logErr("View panicked: %v"), writes a controlled 500, and short-circuits the page document.
  • renderFragment (async re-render, used by flushDirty) — recovers, logs, and returns "" (a no-op prepend, dropped by the drain's elems != "" guard). The dirty-signal flush still proceeds. Covers both the action-autoflush and broadcast-goroutine callers centrally.

Recovery is unconditional (the issue's open question) — mw.Recover stays as the backstop for non-via handlers, plugin endpoints, and custom middleware.

Tests

Built test-first (RYGBA TDD); each confirmed failing for the right reason before implementing (the broadcast test crashed the test binary in Red, as designed):

  • TestView_panicReachesTheConfiguredLoggerNotBareStderr
  • TestView_panicProducesControlled500NotADroppedConnection (also pins: no half-rendered document appended after the 500)
  • TestView_panicDuringReRenderDoesNotEscapeToTheServer
  • TestView_panicDuringReRenderStillFlushesDirtySignals (pins: recover wraps only the render, not the whole flush)
  • TestView_panicInBroadcastReRenderIsRecoveredNotProcessCrashing

Full suite green under go test -race ./... (and -count=20 stress on the panic tests).

joaomdsg added 2 commits May 28, 2026 22:26
View renders had no panic recovery, unlike action handlers and the
OnInit/OnConnect/OnDispose lifecycle guards. A panic in viewFn escaped
to the embedding http.Server: a dropped connection plus a naked stderr
stack on the initial GET, and a crashed process on the broadcast
SyncNow goroutine — with no via log line either way.

Wrap both viewFn callsites: renderView logs and writes a controlled
500 on the GET path; renderFragment logs and drops the fragment on the
async re-render path (action autoflush + broadcast), letting the signal
flush still proceed. Recovery is unconditional; mw.Recover stays as the
backstop for non-via handlers.
@joaomdsg
joaomdsg merged commit cfedfea into main May 28, 2026
4 checks passed
@joaomdsg
joaomdsg deleted the fix/view-recover-48 branch May 28, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

View / lifecycle panics escape the framework — recover should be built in, not opt-in via mw.Recover

1 participant