multica-ai/multica · error

runtime_ids or user identity required

Error message

runtime_ids or user identity required

What it means

HTTP 400 from the daemon WebSocket hub's HandleWebSocket when the resolved client identity carries neither runtime IDs nor a user ID. The hub routes messages per runtime and per user, so a completely anonymous identity cannot be attached to any topic and is rejected before the connection is upgraded.

Source

Thrown at server/internal/daemonws/hub.go:283

		return
	}
	h.kindMu.Lock()
	h.kindRecorder = rec
	h.kindMu.Unlock()
}

func (h *Hub) messageKindRecorder() MessageKindRecorder {
	if h == nil {
		return nil
	}
	h.kindMu.RLock()
	defer h.kindMu.RUnlock()
	return h.kindRecorder
}

func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request, identity ClientIdentity) {
	if len(identity.RuntimeIDs) == 0 && identity.UserID == "" {
		http.Error(w, `{"error":"runtime_ids or user identity required"}`, http.StatusBadRequest)
		return
	}

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

	runtimes := make(map[string]struct{}, len(identity.RuntimeIDs))
	for _, runtimeID := range identity.RuntimeIDs {
		if runtimeID != "" {
			runtimes[runtimeID] = struct{}{}
		}
	}
	c := &client{
		hub:      h,
		conn:     conn,

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Ensure the client authenticates so the hub resolves a UserID, or supplies at least one non-empty runtime ID.
  2. Check any proxy between client and daemon preserves the headers/cookies/params the identity is derived from.
  3. Filter empty strings out of runtime ID lists client-side before connecting.
  4. Verify the daemon auth middleware is enabled — an unauthenticated request yields an empty identity.

Example fix

// before: connect with no identity
dial("ws://daemon/ws") // 400 runtime_ids or user identity required

// after: connect authenticated (cookie/PAT) or with runtime ids
dial("ws://daemon/ws", {headers: {"Authorization": "Bearer " + pat}})
// or
ids := []string{}
for _, id := range runtimeIDs { if id != "" { ids = append(ids, id) } }
dial("ws://daemon/ws?runtime_id=" + strings.Join(ids, ","))
Defensive patterns

Strategy: validation

Validate before calling

func validIdentity(identity ClientIdentity) bool {
    if identity.UserID != "" { return true }
    for _, id := range identity.RuntimeIDs { if id != "" { return true } }
    return false
}
if !validIdentity(id) { return errors.New("refusing to dial: no identity") }

Type guard

func hasIdentity(id ClientIdentity) bool {
    if strings.TrimSpace(id.UserID) != "" { return true }
    for _, r := range id.RuntimeIDs { if strings.TrimSpace(r) != "" { return true } }
    return false
}

Try / catch

conn, resp, err := dialer.Dial(wsURL, headers)
if err != nil && resp != nil && resp.StatusCode == 400 {
    // identity rejected: re-authenticate, then re-dial once
}

Prevention

When it happens

Trigger: Opening a WebSocket to the daemon endpoint with an identity whose RuntimeIDs list is empty (or all blank strings) and whose UserID is empty — e.g. a misconfigured proxy that strips the identity headers/params the hub derives ClientIdentity from, or a client that never authenticates.

Common situations: Auth middleware disabled or bypassed in local dev; a reverse proxy dropping the identity headers; a new client integration that forgot to send runtime identification; runtime IDs sent as empty strings.

Related errors


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