multica-ai/multica · error
workspace_id or workspace_slug required
Error message
workspace_id or workspace_slug required
What it means
HTTP 400 from the realtime WebSocket upgrade handler when the request carries neither workspace_id nor workspace_slug query parameters. The hub is strictly per-workspace — every socket is bound to one workspace's event stream — so it refuses to upgrade a connection that does not name one. resolveSlug being nil also leaves the slug path inert, falling through to this 400.
Source
Thrown at server/internal/realtime/hub.go:781
conn.Close()
}
// HandleWebSocket upgrades an HTTP connection to WebSocket with cookie or
// first-message auth.
func HandleWebSocket(hub *Hub, mc MembershipChecker, pr PATResolver, resolveSlug SlugResolver, w http.ResponseWriter, r *http.Request) {
workspaceID := r.URL.Query().Get("workspace_id")
if workspaceID == "" {
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)View on GitHub (pinned to 2c0912b6ec)
Solutions
- Append ?workspace_id=<uuid> (or ?workspace_slug=<slug>) to the WebSocket URL.
- If using a slug, confirm the server wires a slug resolver; otherwise resolve the ID client-side first.
- Check for typos/casing in the parameter name.
Example fix
// before
const sock = new WebSocket(`ws://${host}/ws`)
// after
const sock = new WebSocket(`ws://${host}/ws?workspace_id=${workspaceId}`) Defensive patterns
Strategy: validation
Validate before calling
function wsUrl(host: string, workspaceId: string): string {
if (!workspaceId) throw new Error('workspaceId required for websocket')
return `ws://${host}/ws?workspace_id=${encodeURIComponent(workspaceId)}`
} Type guard
function canConnect(workspaceId?: string, slug?: string): boolean {
return Boolean(workspaceId && workspaceId.trim()) || Boolean(slug && slug.trim())
} Try / catch
conn, resp, err := dialer.Dial(url, nil)
if err != nil && resp != nil && resp.StatusCode == 400 {
// missing workspace param: fix URL construction, do not retry as-is
} Prevention
- Build socket URLs through a helper that requires a workspace target.
- Assert on required query params in one place client-side.
- Watch for param-name typos (workspaceid vs workspace_id) in code review.
When it happens
Trigger: Opening ws://host/ws with no query params; passing the workspace in a header or body (ignored); a slug supplied while resolveSlug is nil (server wiring without slug support); parameter typo like workspaceid.
Common situations: Frontend builds the socket URL without appending the query string; migration from a global socket API to per-workspace sockets; copy-paste from old client code.
Related errors
- invalid limit
- runtime_ids or user identity required
- workspace not found
- Invalid desktop runtime config JSON: ${err instanceof Error
- repo is not configured for this workspace
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/07e37e79c52fbcad.
Report an issue: GitHub.