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
- 401: re-authenticate with `but config forge auth`
- 403/429: back off, honor Retry-After and X-RateLimit-Reset, then retry
- 5xx: wait and retry; check https://www.githubstatus.com
- 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
- Honor Retry-After and X-RateLimit-Reset headers before retrying
- Batch queries to stay under primary rate limits
- Refresh tokens proactively before long-running jobs
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
- GitHub GraphQL enablePullRequestAutoMerge returned an empty
- GitHub GraphQL disablePullRequestAutoMerge returned an empty
- GitHub GraphQL repository not found for {owner}/{repo}
- GitHub GraphQL pull request #{pr_number} not found for {owne
- GitHub GraphQL pull request #{pr_number} in {owner}/{repo} r
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/36e214088ac7f6f5.
Report an issue: GitHub.