{"record":{"id":"35ff42e31e6c0078","repo":"multica-ai/multica","slug":"runtime-ids-or-user-identity-required","errorCode":null,"errorMessage":"runtime_ids or user identity required","messagePattern":"runtime_ids or user identity required","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"server/internal/daemonws/hub.go","lineNumber":283,"sourceCode":"\t\treturn\n\t}\n\th.kindMu.Lock()\n\th.kindRecorder = rec\n\th.kindMu.Unlock()\n}\n\nfunc (h *Hub) messageKindRecorder() MessageKindRecorder {\n\tif h == nil {\n\t\treturn nil\n\t}\n\th.kindMu.RLock()\n\tdefer h.kindMu.RUnlock()\n\treturn h.kindRecorder\n}\n\nfunc (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request, identity ClientIdentity) {\n\tif len(identity.RuntimeIDs) == 0 && identity.UserID == \"\" {\n\t\thttp.Error(w, `{\"error\":\"runtime_ids or user identity required\"}`, http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tconn, err := h.upgrader.Upgrade(w, r, nil)\n\tif err != nil {\n\t\tslog.Error(\"daemon websocket upgrade failed\", \"error\", err)\n\t\treturn\n\t}\n\n\truntimes := make(map[string]struct{}, len(identity.RuntimeIDs))\n\tfor _, runtimeID := range identity.RuntimeIDs {\n\t\tif runtimeID != \"\" {\n\t\t\truntimes[runtimeID] = struct{}{}\n\t\t}\n\t}\n\tc := &client{\n\t\thub:      h,\n\t\tconn:     conn,","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/daemonws/hub.go#L265-L301","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the client authenticates so the hub resolves a UserID, or supplies at least one non-empty runtime ID.","Check any proxy between client and daemon preserves the headers/cookies/params the identity is derived from.","Filter empty strings out of runtime ID lists client-side before connecting.","Verify the daemon auth middleware is enabled — an unauthenticated request yields an empty identity."],"exampleFix":"// before: connect with no identity\ndial(\"ws://daemon/ws\") // 400 runtime_ids or user identity required\n\n// after: connect authenticated (cookie/PAT) or with runtime ids\ndial(\"ws://daemon/ws\", {headers: {\"Authorization\": \"Bearer \" + pat}})\n// or\nids := []string{}\nfor _, id := range runtimeIDs { if id != \"\" { ids = append(ids, id) } }\ndial(\"ws://daemon/ws?runtime_id=\" + strings.Join(ids, \",\"))","handlingStrategy":"validation","validationCode":"func validIdentity(identity ClientIdentity) bool {\n    if identity.UserID != \"\" { return true }\n    for _, id := range identity.RuntimeIDs { if id != \"\" { return true } }\n    return false\n}\nif !validIdentity(id) { return errors.New(\"refusing to dial: no identity\") }","typeGuard":"func hasIdentity(id ClientIdentity) bool {\n    if strings.TrimSpace(id.UserID) != \"\" { return true }\n    for _, r := range id.RuntimeIDs { if strings.TrimSpace(r) != \"\" { return true } }\n    return false\n}","tryCatchPattern":"conn, resp, err := dialer.Dial(wsURL, headers)\nif err != nil && resp != nil && resp.StatusCode == 400 {\n    // identity rejected: re-authenticate, then re-dial once\n}","preventionTips":["Filter empty strings from runtime ID lists before connecting.","Verify proxies preserve identity headers.","Fail fast client-side when no identity is available instead of dialing."],"tags":["websocket","daemon","authentication","http-400"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}