gitbutlerapp/gitbutler · error · anyhow::Error

GitHub GraphQL request failed: {}

Error message

GitHub GraphQL request failed: {}

What it means

The HTTP POST to GitHub's GraphQL endpoint returned a non-2xx status. This is transport-level failure: bad credentials (401), rate limiting (403/429), or server errors (5xx). Application-level refusals arrive as 200 with an `errors` array and produce the neighboring 'GitHub GraphQL returned errors' message instead.

Source

Thrown at crates/but-github/src/client.rs:1264

        V: Serialize,
    {
        #[derive(Serialize)]
        struct GraphQlRequest<'a, V> {
            query: &'a str,
            variables: &'a V,
        }

        let url = graphql_endpoint_from_base_url(&self.base_url);

        let response = self
            .client
            .post(&url)
            .json(&GraphQlRequest { query, variables })
            .send()
            .await?;

        if !response.status().is_success() {
            bail!("GitHub GraphQL request failed: {}", response.status());
        }

        decode_graphql_response(&response.bytes().await?)
    }
}

/// Decode a GraphQL response body into `T`.
///
/// `data` is typed only once `errors` has been ruled out. GitHub reports a
/// refused mutation as a null field *inside* `data` alongside `errors`, so
/// typing the two together fails on that null and loses the message saying
/// why it was refused.
fn decode_graphql_response<T>(body: &[u8]) -> Result<T>
where
    T: for<'de> Deserialize<'de>,
{
    #[derive(Deserialize)]
    struct GraphQlError {

View on GitHub (pinned to caf1f223d3)

Solutions

  1. 401: re-authenticate with `but config forge auth`
  2. 403/429: back off, honor Retry-After and X-RateLimit-Reset, then retry
  3. 5xx: wait and retry; check https://www.githubstatus.com
  4. Verify the forge base URL configuration

Example fix

// before
let data = client.graphql_query(QUERY, &vars).await?;

// after
let data = retryable(|attempt| async {
    let backoff = Duration::from_secs(2u64.saturating_pow(attempt));
    tokio::time::sleep(backoff).await;
    client.graphql_query(QUERY, &vars).await
}, 4).await?;
Defensive patterns

Strategy: retry

Try / catch

for attempt in 0..4 {
    match client.graphql_query(query, &vars).await {
        Ok(data) => break Ok(data),
        Err(e) if e.to_string().contains("401") => break Err(e.context("re-authenticate: but config forge auth")),
        Err(e) if e.to_string().contains("403") || e.to_string().contains("429") => {
            tokio::time::sleep(exp_backoff(attempt)).await
        }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: Expired or revoked token (401); primary rate limit exhausted, IP allow-listing, or SAML enforcement (403); secondary rate limit with a Retry-After header (429); GitHub incident (5xx); a base_url so wrong the POST hits a server that is not the API.

Common situations: Long-running CLIs polling without rate-limit handling; users revoking the OAuth app; enterprise proxies; github.com outages.

Related errors


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