zeroclaw-labs/zeroclaw · critical · anyhow::Error

createSession failed ({status}): {body}

Error message

createSession failed ({status}): {body}

What it means

Bluesky's com.atproto.server.createSession returned a non-2xx response while exchanging the configured handle + app password for session JWTs; the HTTP status and response body are embedded verbatim. listen() calls create_session first, so this error typically aborts channel startup — a 401 body saying the identifier or password is invalid means bad credentials.

Source

Thrown at crates/zeroclaw-channels/src/bluesky.rs:140

    /// Create a new session with handle + app password.
    async fn create_session(&self) -> Result<()> {
        let client = self.http_client();
        let resp = client
            .post(format!("{BSKY_API_BASE}/com.atproto.server.createSession"))
            .json(&serde_json::json!({
                "identifier": self.handle,
                "password": self.app_password,
            }))
            .send()
            .await?;

        let status = resp.status();
        if !status.is_success() {
            let body = resp
                .text()
                .await
                .unwrap_or_else(|e| format!("<failed to read response: {e}>"));
            bail!("createSession failed ({status}): {body}");
        }

        let session: CreateSessionResponse = resp.json().await?;
        let mut auth = self.auth.lock();
        auth.access_jwt = session.access_jwt;
        auth.refresh_jwt = session.refresh_jwt;
        auth.did = session.did;
        // AT Protocol JWTs typically last ~2 hours; refresh well before that.
        auth.expires_at = Instant::now() + Duration::from_secs(90 * 60);
        Ok(())
    }

    /// Refresh an existing session.
    async fn refresh_session(&self) -> Result<()> {
        let refresh_jwt = {
            let auth = self.auth.lock();
            auth.refresh_jwt.clone()
        };

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Create or verify an App Password in Bluesy settings (Settings -> App passwords) and set it as the channel's app_password.
  2. Confirm the handle is exactly right, including any custom domain.
  3. Read the embedded status: 401 means credentials, 400 means malformed identifier, 429 means rate limited (back off and retry).
  4. Reproduce outside ZeroClaw with a manual createSession request against bsky.social/xrpc using the same credentials to confirm whether they are valid.

Example fix

# before: main login password is rejected by AT Protocol
[channels.bluesky.main]
handle = "me.example.com"
app_password = "my-login-password"

# after: App Password generated in Bluesky settings
[channels.bluesky.main]
handle = "me.example.com"
app_password = "xxxx-xxxx-xxxx-xxxx"
Defensive patterns

Strategy: try-catch

Validate before calling

if handle.trim().is_empty() || app_password.trim().is_empty() {
    anyhow::bail!("bluesky channel requires a non-empty handle and app password");
}

Try / catch

if let Err(e) = bluesky.listen(tx).await {
    if e.to_string().contains("createSession failed (401") {
        tracing::error!("bluesky credentials rejected — check the app password and handle");
    }
    return Err(e);
}

Prevention

When it happens

Trigger: BlueskyChannel::listen (or any first-auth path) with a wrong handle, a revoked or rotated app password, the account's main login password instead of an App Password, or the API rejecting the createSession request (rate limit, malformed identifier).

Common situations: Using the login password instead of an App Password created in Bluesky settings; app password regenerated after a leak so the old one is dead; handle typo or renamed account; polling too aggressively and hitting rate limits at startup.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/8ae3aaad4751d4fd. Report an issue: GitHub.