multica-ai/multica · error

errMsg

Error message

errMsg

What it means

HTTP 401 from the realtime WebSocket upgrade handler when the auth cookie is present but authenticateToken rejects its value. authenticateToken validates the cookie's token (PAT or JWT) and returns a non-empty error message on failure — expired session, revoked token, wrong signing secret, or malformed token — and the handler passes that message straight into http.Error. Note this path does not run CSRF validation, unlike the HTTP middleware.

Source

Thrown at server/internal/realtime/hub.go:789

		if slug := r.URL.Query().Get("workspace_slug"); slug != "" && resolveSlug != nil {
			resolved, err := resolveSlug(r.Context(), slug)
			if err != nil {
				http.Error(w, `{"error":"workspace not found"}`, http.StatusNotFound)
				return
			}
			workspaceID = resolved
		}
	}
	if workspaceID == "" {
		http.Error(w, `{"error":"workspace_id or workspace_slug required"}`, http.StatusBadRequest)
		return
	}

	var userID string
	if cookie, err := r.Cookie(auth.AuthCookieName); err == nil && cookie.Value != "" {
		uid, errMsg := authenticateToken(cookie.Value, pr, r.Context())
		if errMsg != "" {
			http.Error(w, errMsg, http.StatusUnauthorized)
			return
		}
		if !mc.IsMember(r.Context(), uid, workspaceID) {
			http.Error(w, `{"error":"not a member of this workspace"}`, http.StatusForbidden)
			return
		}
		userID = uid
	}

	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		slog.Error("websocket upgrade failed", "error", err)
		return
	}

	// Bound inbound messages here rather than in readPump: the token auth
	// path below reads its first frame before the caller is authenticated, so
	// a limit installed any later leaves that read unbounded.

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Re-authenticate in the browser (log in again) to get a fresh cookie, then reconnect the socket.
  2. If reconnect loops with 401, stop the loop and prompt re-login instead of hammering the endpoint.
  3. After rotating JWT_SECRET, expect all cookie sessions to invalidate — plan a re-login wave.
  4. Alternatively connect with a PAT via the token flow if the client supports it.

Example fix

// before: blind reconnect loop
socket.onclose = () => setTimeout(connect, 1000) // 401 loop

// after: stop and re-auth on 401
socket.onclose = (e) => {
  if (e.code === 1008 || lastStatus === 401) { window.location = '/login'; return }
  setTimeout(connect, 1000)
}
Defensive patterns

Strategy: fallback

Validate before calling

// before connecting, cheap-check the session via REST
resp, _ := http.Get(base + "/api/me")
if resp.StatusCode == 401 { await relogin(); /* fresh cookie, then connect */ }

Try / catch

conn, resp, err := dialer.Dial(wsURL, cookieHeader)
if err != nil && resp != nil && resp.StatusCode == 401 {
    if ok := refreshSession(); !ok { promptLogin(); return }
    conn, _, err = dialer.Dial(wsURL, cookieHeader) // one retry with fresh cookie
}

Prevention

When it happens

Trigger: Connecting to /ws with an auth cookie whose token is expired, revoked, signed with a different secret (server restart rotated JWT_SECRET), or corrupt; browser auto-sends a stale cookie after session expiry.

Common situations: Session expired while the tab stayed open and the socket reconnect logic keeps retrying; server secret rotation invalidating all cookies; cookie truncated by size limits; mixed environments (staging cookie sent to prod).

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/43b28ca452da6031. Report an issue: GitHub.