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
- Create or verify an App Password in Bluesy settings (Settings -> App passwords) and set it as the channel's app_password.
- Confirm the handle is exactly right, including any custom domain.
- Read the embedded status: 401 means credentials, 400 means malformed identifier, 429 means rate limited (back off and retry).
- 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
- Always use App Passwords, never the account login password
- Store app passwords in the secret store and rotate without code changes
- Do one createSession at startup and fail fast on 401 instead of retry-looping
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
- post failed ({status}): {body}
- Jira myself failed ({status}): {}
- tenant_access_token failed: {msg}
- matrix: configure either `access_token` or `password`
- Nextcloud Talk: no bot secret configured (set bot_token or w
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8ae3aaad4751d4fd.
Report an issue: GitHub.