dani-garcia/vaultwarden · error

Error rotating organization API Key

Error message

Error rotating organization API Key

What it means

A Rust .expect() panic raised when OrganizationApiKey::save returns Err during key rotation: the UPDATE write to the organization_api_key table failed at the database level. It sits in the api_key handler (organizations.rs:3198) behind POST /organizations/<org_id>/rotate-api-key, after AdminHeaders auth and PasswordOrOtpData validation (data.validate at organizations.rs:3212) have already passed. Because Rocket catches the panic per-request, the client typically gets a 500 instead of a structured error, and the in-memory key has already been regenerated while the DB row may still hold the old key.

Solutions

  1. Check the server logs for the underlying diesel QueryResult error (connection refused, constraint name, 'attempt to write a readonly database') — that message names the real cause.
  2. Verify DB health and permissions: for SQLite check write permission on the db file and its directory; for PostgreSQL/MySQL check connectivity, disk space, and that the DB user can UPDATE organization_api_key.
  3. Ensure database migrations have been run for the current version so the organization_api_key schema matches the model.
  4. Replace .expect() with proper error propagation (return a 500 JsonResult via the project's error handling) so clients get a response and the server keeps serving other requests.

Example fix

// before
org_api_key.save(&conn).await.expect("Error rotating organization API Key");

// after (propagate as an API error instead of panicking)
org_api_key.save(&conn).await.map_err(|e| {
    error!("Error rotating organization API key for {}: {:#?}", org_id, e);
    Error::new("Error rotating organization API key", "Failed to save the rotated API key")
})?;
Defensive patterns

Strategy: validation

Validate before calling

-- Confirm the row is writable and the schema is current before rotating
SELECT org_uuid, revision_date FROM organization_api_key WHERE org_uuid = '<org_id>';
-- then verify the app user can UPDATE it (PostgreSQL example)
SELECT has_table_privilege(current_user, 'organization_api_key', 'UPDATE');

Try / catch

// Recommended: propagate the error as a handled API error instead of expect()
org_api_key
    .save(&conn)
    .await
    .map_err(|e| {
        error!("Error rotating organization API key for {}: {:#?}", org_id, e);
        Error::new("Error rotating organization API key", "Database write failed")
    })?;

Prevention

When it happens

Trigger: An org admin calls POST /organizations/<org_id>/rotate-api-key with valid password/OTP, an organization_api_key row already exists for that org (the find_by_org_uuid Some branch), rotate=true — and the subsequent UPDATE fails: database down/connection dropped, table locked, constraint violation, read-only DB file, or schema drift after a skipped migration.

Common situations: SQLite file permissions or a read-only mount (common in Docker when the data dir is mis-mounted); PostgreSQL/MySQL restarted mid-request; migrations not applied so column types/constraints mismatch the model; unique-constraint collisions on org_uuid; disk full.

Related errors


AI-assisted analysis of dani-garcia/vaultwarden@6729e83521 (2026-08-16). Data as JSON: /api/errors/f32b31a7fda6a77f. Report an issue: GitHub.

Appendix: source

Thrown at src/api/core/organizations.rs:3218

    data: Json<PasswordOrOtpData>,
    rotate: bool,
    headers: AdminHeaders,
    conn: DbConn,
) -> JsonResult {
    if org_id != &headers.org_id {
        err!("Organization not found", "Organization id's do not match");
    }
    let data: PasswordOrOtpData = data.into_inner();
    let user = headers.user;

    // Validate the admin users password/otp
    data.validate(&user, true, &conn).await?;

    let org_api_key = if let Some(mut org_api_key) = OrganizationApiKey::find_by_org_uuid(org_id, &conn).await {
        if rotate {
            org_api_key.api_key = crate::crypto::generate_api_key();
            org_api_key.revision_date = chrono::Utc::now().naive_utc();
            org_api_key.save(&conn).await.expect("Error rotating organization API Key");
        }
        org_api_key
    } else {
        let api_key = crate::crypto::generate_api_key();
        let new_org_api_key = OrganizationApiKey::new(org_id.clone(), api_key);
        new_org_api_key.save(&conn).await.expect("Error creating organization API Key");
        new_org_api_key
    };

    Ok(Json(json!({
      "apiKey": org_api_key.api_key,
      "revisionDate": crate::util::format_date(&org_api_key.revision_date),
      "object": "apiKey",
    })))
}

#[post("/organizations/<org_id>/api-key", data = "<data>")]
async fn post_api_key(

View on GitHub (pinned to 6729e83521)