tinyhumansai/openhuman · error

integrationId must be a 24-char hex id

Error message

integrationId must be a 24-char hex id

What it means

Thrown by fetch_integration_tokens_handoff when the integration id is empty or not exactly 24 chars after trimming. Backend integration ids are MongoDB ObjectIds (24 hex chars) and the id is interpolated into auth/integrations/{id}/tokens. Note the code only checks length — the "hex" in the message is not verified, so a 24-char non-hex string passes here and fails server-side.

Source

Thrown at src/api/rest.rs:879

        let integrations = value
            .get("integrations")
            .cloned()
            .unwrap_or_else(|| value.clone());
        serde_json::from_value(integrations).context("parse integrations response")
    }

    /// Fetches the decrypted OAuth tokens for a specific integration.
    ///
    /// This is a one-time handoff process. The encryption key must match the
    /// one used by the backend to encrypt the tokens.
    pub async fn fetch_integration_tokens_handoff(
        &self,
        integration_id: &str,
        bearer_jwt: &str,
        encryption_key: &str,
    ) -> Result<IntegrationTokensHandoff> {
        let id = integration_id.trim();
        anyhow::ensure!(
            !id.is_empty() && id.len() == 24,
            "integrationId must be a 24-char hex id"
        );
        let body = serde_json::json!({ "key": encryption_key.trim() });
        let value = self
            .authed_json(
                bearer_jwt,
                Method::POST,
                &format!("auth/integrations/{id}/tokens"),
                Some(body),
            )
            .await
            .context("integration tokens handoff")?;
        let encrypted = value
            .get("encrypted")
            .and_then(Value::as_str)
            .context("integration tokens response missing encrypted payload")?;
        let plaintext = decrypt_handoff_blob(encrypted, encryption_key.trim())?;

View on GitHub (pinned to a221052e0d)

Solutions

  1. Use the id verbatim from list_integrations — that is the backend ObjectId
  2. If you hold your own ids, map them to the backend integration id before calling
  3. Validate 24-char length (and hex-ness, to be strict) at the boundary

Example fix

// before
let h = client.fetch_integration_tokens_handoff(&my_uuid, &jwt, &key).await?;

// after
let id = list_integrations(&jwt).await?
    .into_iter().find(|i| i.provider == wanted)
    .map(|i| i.id)
    .ok_or_else(|| anyhow!("no integration for {wanted}"))?;
let h = client.fetch_integration_tokens_handoff(&id, &jwt, &key).await?;
Defensive patterns

Strategy: validation

Validate before calling

let id = integration_id.trim();
anyhow::ensure!(id.len() == 24, "integration id must be a 24-char backend ObjectId, got {} chars", id.len());
let h = client.fetch_integration_tokens_handoff(id, &jwt, &key).await?;

Type guard

fn is_backend_integration_id(s: &str) -> bool {
    let t = s.trim();
    t.len() == 24 && t.chars().all(|c| c.is_ascii_hexdigit())
}

Prevention

When it happens

Trigger: Passing a UUID (32/36 chars), an internal numeric id, or a blank string as integration_id — e.g. mixing up your own record key with the backend's integration ObjectId.

Common situations: Ids from two systems conflated after a refactor, trailing newline/whitespace in ids read from CSV or clipboard, or a placeholder like "test" used in a scratch script.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/78d04800ae906058. Report an issue: GitHub.