{"record":{"id":"58290a47c23caa5c","repo":"gitbutlerapp/gitbutler","slug":"github-returned-an-error","errorCode":null,"errorMessage":"GitHub returned an error: {} ({})","messagePattern":"GitHub returned an error: (.+?) \\((.+?)\\)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-github/src/lib.rs","lineNumber":40,"sourceCode":"\n#[cfg_attr(feature = \"export-schema\", derive(schemars::JsonSchema))]\n#[derive(Debug, Deserialize, Serialize, Clone, Default)]\npub struct Verification {\n    pub user_code: String,\n    pub device_code: String,\n}\n#[cfg(feature = \"export-schema\")]\nbut_schemars::register_sdk_type!(Verification);\n\n/// 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.\nfn parse_github_oauth_response<T: serde::de::DeserializeOwned>(body: &str) -> Result<T> {\n    let value: serde_json::Value =\n        serde_json::from_str(body).context(\"Response body was not valid JSON\")?;\n    if let Some(error) = value.get(\"error\").and_then(serde_json::Value::as_str) {\n        let description = value\n            .get(\"error_description\")\n            .and_then(serde_json::Value::as_str);\n        anyhow::bail!(\n            \"GitHub returned an error: {} ({})\",\n            error,\n            description.unwrap_or(\"no description\"),\n        );\n    }\n    serde_json::from_value(value).context(\"Response body did not match expected schema\")\n}\n\npub async fn init_github_device_oauth() -> Result<Verification> {\n    let mut req_body = HashMap::new();\n    let app_settings = AppSettings::load_from_default_path_creating_without_customization()?;\n    let client_id = app_settings.github_oauth_app.oauth_client_id.clone();\n    req_body.insert(\"client_id\", client_id.as_str());\n    req_body.insert(\"scope\", \"repo\");\n\n    let mut headers = reqwest::header::HeaderMap::new();\n    headers.insert(\n        reqwest::header::ACCEPT,","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-github/src/lib.rs#L22-L58","documentation":"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.","triggerScenarios":"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).","commonSituations":"Polling the token endpoint too eagerly during device flow; org-restricted OAuth apps; misconfigured OAuth client ids in app settings.","solutions":["authorization_pending/slow_down: keep polling with the interval GitHub told you - this is normal device flow, not a failure","device_flow_disabled: use an OAuth app that permits device flow","access_denied: restart the flow and approve the consent screen","incorrect_client_credentials: fix the OAuth app client id in settings"],"exampleFix":"// before\nlet token = poll_device_token(&verification).await?;\n\n// after\nmatch poll_device_token(&verification).await {\n    Err(e) if e.to_string().contains(\"authorization_pending\") => {\n        tokio::time::sleep(interval).await;\n        continue;\n    }\n    Err(e) if e.to_string().contains(\"slow_down\") => {\n        interval = interval.mul_f64(1.5);\n        tokio::time::sleep(interval).await;\n        continue;\n    }\n    other => other?,\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_retryable_oauth_error(err: &anyhow::Error) -> bool {\n    let msg = err.to_string();\n    msg.contains(\"authorization_pending\") || msg.contains(\"slow_down\")\n}","tryCatchPattern":"loop {\n    match poll_device_flow_token(&verification).await {\n        Ok(token) => break Ok(token),\n        Err(e) if e.to_string().contains(\"authorization_pending\") => {\n            tokio::time::sleep(interval).await\n        }\n        Err(e) if e.to_string().contains(\"slow_down\") => {\n            interval = interval.mul_f64(1.5);\n            tokio::time::sleep(interval).await\n        }\n        Err(e) => break Err(e), // access_denied, device_flow_disabled, ... are terminal\n    }\n}","preventionTips":["Poll only at the interval GitHub returns, never faster","Treat only authorization_pending and slow_down as retryable device-flow codes","Fix the OAuth client id before shipping if you see incorrect_client_credentials"],"tags":["github","oauth","device-flow","authentication"],"backgroundTag":"oauth-device-flow-error","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-09-01T03:17:15.561Z"}