gitbutlerapp/gitbutler · error · anyhow::Error

GitHub returned an error: {} ({})

Error message

GitHub returned an error: {} ({})

What it means

GitHub's OAuth endpoints signal failures by returning JSON with an `error` code (plus `error_description`) instead of the expected payload. This parser detects that shape before deserializing, so the real code - device_flow_disabled, authorization_pending, access_denied, incorrect_client_credentials - surfaces instead of a generic serde 'missing field' error.

Source

Thrown at crates/but-github/src/lib.rs:40

#[cfg_attr(feature = "export-schema", derive(schemars::JsonSchema))]
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Verification {
    pub user_code: String,
    pub device_code: String,
}
#[cfg(feature = "export-schema")]
but_schemars::register_sdk_type!(Verification);

/// Detect GitHub's OAuth error shape (e.g. `device_flow_disabled`, `authorization_pending`) before falling back to the expected payload, so the real cause surfaces instead of a generic "missing field" serde error.
fn parse_github_oauth_response<T: serde::de::DeserializeOwned>(body: &str) -> Result<T> {
    let value: serde_json::Value =
        serde_json::from_str(body).context("Response body was not valid JSON")?;
    if let Some(error) = value.get("error").and_then(serde_json::Value::as_str) {
        let description = value
            .get("error_description")
            .and_then(serde_json::Value::as_str);
        anyhow::bail!(
            "GitHub returned an error: {} ({})",
            error,
            description.unwrap_or("no description"),
        );
    }
    serde_json::from_value(value).context("Response body did not match expected schema")
}

pub async fn init_github_device_oauth() -> Result<Verification> {
    let mut req_body = HashMap::new();
    let app_settings = AppSettings::load_from_default_path_creating_without_customization()?;
    let client_id = app_settings.github_oauth_app.oauth_client_id.clone();
    req_body.insert("client_id", client_id.as_str());
    req_body.insert("scope", "repo");

    let mut headers = reqwest::header::HeaderMap::new();
    headers.insert(
        reqwest::header::ACCEPT,

View on GitHub (pinned to caf1f223d3)

Solutions

  1. authorization_pending/slow_down: keep polling with the interval GitHub told you - this is normal device flow, not a failure
  2. device_flow_disabled: use an OAuth app that permits device flow
  3. access_denied: restart the flow and approve the consent screen
  4. incorrect_client_credentials: fix the OAuth app client id in settings

Example fix

// before
let token = poll_device_token(&verification).await?;

// after
match poll_device_token(&verification).await {
    Err(e) if e.to_string().contains("authorization_pending") => {
        tokio::time::sleep(interval).await;
        continue;
    }
    Err(e) if e.to_string().contains("slow_down") => {
        interval = interval.mul_f64(1.5);
        tokio::time::sleep(interval).await;
        continue;
    }
    other => other?,
}
Defensive patterns

Strategy: retry

Type guard

fn is_retryable_oauth_error(err: &anyhow::Error) -> bool {
    let msg = err.to_string();
    msg.contains("authorization_pending") || msg.contains("slow_down")
}

Try / catch

loop {
    match poll_device_flow_token(&verification).await {
        Ok(token) => break Ok(token),
        Err(e) if e.to_string().contains("authorization_pending") => {
            tokio::time::sleep(interval).await
        }
        Err(e) if e.to_string().contains("slow_down") => {
            interval = interval.mul_f64(1.5);
            tokio::time::sleep(interval).await
        }
        Err(e) => break Err(e), // access_denied, device_flow_disabled, ... are terminal
    }
}

Prevention

When it happens

Trigger: Device-flow token polling before the user approves (authorization_pending, slow_down); an OAuth app with device flow disabled (device_flow_disabled); the user denies the consent screen (access_denied); a wrong oauth_client_id in settings (incorrect_client_credentials).

Common situations: Polling the token endpoint too eagerly during device flow; org-restricted OAuth apps; misconfigured OAuth client ids in app settings.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/58290a47c23caa5c. Report an issue: GitHub.