{"record":{"id":"43b28ca452da6031","repo":"multica-ai/multica","slug":"errmsg","errorCode":null,"errorMessage":"errMsg","messagePattern":"errMsg","errorType":"http","errorClass":null,"httpStatus":401,"severity":"error","filePath":"server/internal/realtime/hub.go","lineNumber":789,"sourceCode":"\t\tif slug := r.URL.Query().Get(\"workspace_slug\"); slug != \"\" && resolveSlug != nil {\n\t\t\tresolved, err := resolveSlug(r.Context(), slug)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, `{\"error\":\"workspace not found\"}`, http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tworkspaceID = resolved\n\t\t}\n\t}\n\tif workspaceID == \"\" {\n\t\thttp.Error(w, `{\"error\":\"workspace_id or workspace_slug required\"}`, http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar userID string\n\tif cookie, err := r.Cookie(auth.AuthCookieName); err == nil && cookie.Value != \"\" {\n\t\tuid, errMsg := authenticateToken(cookie.Value, pr, r.Context())\n\t\tif errMsg != \"\" {\n\t\t\thttp.Error(w, errMsg, http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\t\tif !mc.IsMember(r.Context(), uid, workspaceID) {\n\t\t\thttp.Error(w, `{\"error\":\"not a member of this workspace\"}`, http.StatusForbidden)\n\t\t\treturn\n\t\t}\n\t\tuserID = uid\n\t}\n\n\tconn, err := upgrader.Upgrade(w, r, nil)\n\tif err != nil {\n\t\tslog.Error(\"websocket upgrade failed\", \"error\", err)\n\t\treturn\n\t}\n\n\t// Bound inbound messages here rather than in readPump: the token auth\n\t// path below reads its first frame before the caller is authenticated, so\n\t// a limit installed any later leaves that read unbounded.","sourceCodeStart":771,"sourceCodeEnd":807,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/realtime/hub.go#L771-L807","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Re-authenticate in the browser (log in again) to get a fresh cookie, then reconnect the socket.","If reconnect loops with 401, stop the loop and prompt re-login instead of hammering the endpoint.","After rotating JWT_SECRET, expect all cookie sessions to invalidate — plan a re-login wave.","Alternatively connect with a PAT via the token flow if the client supports it."],"exampleFix":"// before: blind reconnect loop\nsocket.onclose = () => setTimeout(connect, 1000) // 401 loop\n\n// after: stop and re-auth on 401\nsocket.onclose = (e) => {\n  if (e.code === 1008 || lastStatus === 401) { window.location = '/login'; return }\n  setTimeout(connect, 1000)\n}","handlingStrategy":"fallback","validationCode":"// before connecting, cheap-check the session via REST\nresp, _ := http.Get(base + \"/api/me\")\nif resp.StatusCode == 401 { await relogin(); /* fresh cookie, then connect */ }","typeGuard":null,"tryCatchPattern":"conn, resp, err := dialer.Dial(wsURL, cookieHeader)\nif err != nil && resp != nil && resp.StatusCode == 401 {\n    if ok := refreshSession(); !ok { promptLogin(); return }\n    conn, _, err = dialer.Dial(wsURL, cookieHeader) // one retry with fresh cookie\n}","preventionTips":["Verify the session via a lightweight REST call before opening sockets.","On 401, re-authenticate once and stop — never auto-loop reconnects with a dead cookie.","Plan re-login flows after JWT_SECRET rotations; cookies die with the old secret."],"tags":["websocket","authentication","http-401","cookies","session"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}