wavetermdev/waveterm · critical

internal server error: %v

Error message

internal server error: %v

What it means

httpHandlers.handleRender (tsunami/engine/serverhandlers.go:105) recovers any panic in the render handler via util.PanicHandler and returns HTTP 500 with 'internal server error: <panic>'. This is a crash of the VDom render pipeline, not a client error.

Source

Thrown at tsunami/engine/serverhandlers.go:105

	mux.HandleFunc("/api/terminput", h.handleTermInput)
	mux.HandleFunc("/dyn/", h.handleDynContent)

	// Add handler for static files at /static/ path
	if opts.StaticFS != nil {
		mux.HandleFunc("/static/", h.handleStaticPathFiles(opts.StaticFS))
	}

	// Add fallback handler for embedded static files in production mode
	if opts.AssetsFS != nil {
		mux.HandleFunc("/", h.handleStaticFiles(opts.AssetsFS))
	}
}

func (h *httpHandlers) handleRender(w http.ResponseWriter, r *http.Request) {
	defer func() {
		panicErr := util.PanicHandler("handleRender", recover())
		if panicErr != nil {
			http.Error(w, fmt.Sprintf("internal server error: %v", panicErr), http.StatusInternalServerError)
		}
	}()

	setNoCacheHeaders(w)

	if r.Method != http.MethodPost {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, fmt.Sprintf("failed to read request body: %v", err), http.StatusBadRequest)
		return
	}

	var feUpdate rpctypes.VDomFrontendUpdate
	if err := json.Unmarshal(body, &feUpdate); err != nil {

View on GitHub (pinned to a4447c1563)

Solutions

  1. Check server logs for the PanicHandler output — it contains the panic message and stack trace identifying the failing code path.
  2. Reproduce with the specific VDomFrontendUpdate payload that triggered the crash and fix the nil/unsafe access in the render handler.
  3. Add nil/len guards around model or props access in the render path.
  4. Retry the render request after the fix; each request runs in its own goroutine so the server itself survives.

Example fix

// before
node := renderTree[update.NodeId]
applyUpdate(node, update) // panics when node is nil

// after
node, ok := renderTree[update.NodeId]
if !ok {
    log.Printf("unknown node %q", update.NodeId)
    return
}
applyUpdate(node, update)
Defensive patterns

Strategy: retry

Validate before calling

// sanity-check the update payload before posting so the renderer never sees unexpected shapes
if (feUpdate == null || typeof feUpdate !== "object") throw new Error("invalid VDomFrontendUpdate");

Type guard

function isFeUpdate(u) {
  return u != null && typeof u === "object" && "NodeId" in u;
}

Try / catch

const resp = await fetch(renderUrl, {method: "POST", body: JSON.stringify(feUpdate)});
if (resp.status === 500) {
  const msg = await resp.text();
  if (msg.startsWith("internal server error")) {
    return retryWithBackoff(() => postRender(feUpdate)); // server survived; transient panic may pass on retry
  }
  throw new Error(msg);
}

Prevention

When it happens

Trigger: A panic occurs anywhere inside handleRender after the deferred recover is installed — e.g. nil dereference in the render model, panicking renderer plugin, or index-out-of-range while processing a VDomFrontendUpdate.

Common situations: Malformed but structurally-valid JSON updates driving the renderer into an unexpected state, race conditions in engine state accessed by concurrent render requests, or a newly added render component that panics on nil props.

Understand the failure class

Related errors


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/42ab1d16166917eb. Report an issue: GitHub.