multica-ai/multica · error · lark.ErrInstallationNotFound

ErrInstallationNotFound

ErrInstallationNotFound

Error message

lark installation not found

What it means

Lark ErrInstallationNotFound: no lark installation row matches the lookup in the given workspace. It is a distinct sentinel (not raw pgx.ErrNoRows) so HTTP handlers can map it straight to 404 without importing pgx.

Source

Thrown at server/internal/integrations/lark/installation.go:134

		if errors.Is(err, pgx.ErrNoRows) {
			return Installation{}, ErrInstallationNotFound
		}
		return Installation{}, err
	}
	return row, nil
}

// ListByWorkspace returns every installation rooted at the workspace,
// active and revoked, oldest first. The status column lets the UI
// distinguish "wired up" from "torn down but kept for audit".
func (s *InstallationService) ListByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]Installation, error) {
	return s.queries.ListLarkInstallationsByWorkspace(ctx, workspaceID)
}

// ErrInstallationNotFound surfaces "no row matches in this workspace"
// — used by the HTTP layer to return 404. Distinct from a plain
// pgx.ErrNoRows so handlers do not need to import pgx.
var ErrInstallationNotFound = errors.New("lark installation not found")

func validateInstallationParams(p InstallationParams) error {
	switch {
	case !p.WorkspaceID.Valid:
		return errors.New("workspace_id is required")
	case !p.AgentID.Valid:
		return errors.New("agent_id is required")
	case !p.InstallerUserID.Valid:
		return errors.New("installer_user_id is required")
	case p.AppID == "":
		return errors.New("app_id is required")
	case p.AppSecret == "":
		return errors.New("app_secret is required")
	case p.BotOpenID == "":
		return errors.New("bot_open_id is required")
	}
	return nil
}

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Re-list installations via ListByWorkspace for the workspace and use the current installation id from that result.
  2. Verify the workspace_id parameter matches the workspace the installation was created in.
  3. If the row was revoked, treat it as gone for actionable paths and re-run the lark install flow to create a new installation.

Example fix

// before
inst, err := svc.Get(ctx, workspaceID, installationID)
if err != nil {
	log.Fatal(err) // raw no-rows leaks to caller
}

// after
inst, err := svc.Get(ctx, workspaceID, installationID)
if errors.Is(err, lark.ErrInstallationNotFound) {
	http.NotFound(w, r)
	return
}
Defensive patterns

Strategy: try-catch

Validate before calling

insts, err := svc.ListByWorkspace(ctx, workspaceID)
if err != nil { return err }
known := map[pgtype.UUID]bool{}
for _, i := range insts { known[i.ID] = true }
if !known[wantID] {
	// refresh UI instead of a doomed get
	return respondList(insts)
}

Try / catch

inst, err := svc.Get(ctx, workspaceID, id)
if err != nil {
	if errors.Is(err, lark.ErrInstallationNotFound) {
		http.NotFound(w, r) // client refetches the list
		return
	}
	return err
}

Prevention

When it happens

Trigger: Calling InstallationService Get/Revoke-style lookups (e.g. GetLarkInstallationInWorkspace) with a workspace_id + installation_id pair that has no row, or an installation that was revoked/removed. Distinct from ListByWorkspace, which returns both active and revoked installations and never errors on empties.

Common situations: Stale URL or cached UI referencing an installation deleted after a workspace re-setup; wrong workspace parameter (installation exists but in another workspace); revoked installation being addressed through a get-by-id path instead of the list view.

Related errors


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