{"record":{"id":"392ad1bbaf26bd2f","repo":"tonhowtf/omniget","slug":"thread-392ad1","errorCode":null,"errorMessage":"{} / {}","messagePattern":"\\{\\} / \\{\\}","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/omniget-core/src/core/tools/x/thread.rs","lineNumber":55,"sourceCode":"    match super::fx::thread(&id).await {\n        Ok((focal, posts, truncated)) => {\n            let posts = if posts.is_empty() {\n                vec![focal.clone()]\n            } else {\n                posts\n            };\n            Ok(Thread {\n                focal,\n                posts,\n                truncated,\n                source: \"fxtwitter\".into(),\n            })\n        }\n        Err(fx_err) => {\n            tracing::info!(\"[x] fxtwitter falhou ({}), tentando GraphQL\", fx_err);\n            unroll_graphql(&id)\n                .await\n                .map_err(|e| anyhow!(\"{} / {}\", fx_err, e))\n        }\n    }\n}\n\nasync fn unroll_graphql(id: &str) -> anyhow::Result<Thread> {\n    let client = XClient::new()?;\n    let mut all: Vec<XPost> = Vec::new();\n    let mut cursor: Option<String> = None;\n    for _ in 0..4 {\n        let mut vars = json!({\n            \"focalTweetId\": id,\n            \"with_rux_injections\": false,\n            \"rankingMode\": \"Relevance\",\n            \"includePromotedContent\": false,\n            \"withCommunity\": true,\n            \"withQuickPromoteEligibilityTweetFields\": true,\n            \"withBirdwatchNotes\": true,\n            \"withVoice\": true,","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/tools/x/thread.rs#L37-L73","documentation":"unroll first tries the fxtwitter API, then falls back to GraphQL; if BOTH fail it throws `{fx_err} / {graphql_err}`, concatenating the two underlying errors. This is a wrapper error: it means the thread could not be fetched from either backend, and both root causes are preserved in the single message.","triggerScenarios":"Calling unroll when fxtwitter returns an error (tweet deleted/NSFW-gated, network failure, fxtwitter outage) AND the GraphQL fallback also fails (guest token rejected, rate limit 429, GraphQL schema/auth change, network error).","commonSituations":"Deleted or protected tweets (both backends 404); fxtwitter API downtime combined with X's GraphQL guest-token rotation failing; heavy rate limiting after bulk unrolls; Twitter changing GraphQL query IDs causing the fallback to error.","solutions":["Read both halves of the message: fix each root cause separately (fxtwitter side vs GraphQL side)","For 429/rate-limit causes, back off and retry after a delay; for deleted/protected tweets, surface a user-facing 'tweet unavailable' instead of retrying","Check connectivity/proxy settings; if GraphQL repeatedly fails due to X API changes, update the omniget-core GraphQL query definitions"],"exampleFix":"// before\nunroll_graphql(&id)\n    .await\n    .map_err(|e| anyhow!(\"{} / {}\", fx_err, e))\n// after\nunroll_graphql(&id).await.map_err(|e| {\n    let combined = format!(\"fxtwitter: {fx_err}; graphql: {e}\");\n    if fx_err.to_string().contains(\"404\") && e.to_string().contains(\"404\") {\n        anyhow!(\"post do X nao disponivel (removido ou privado)\")\n    } else {\n        anyhow!(combined)\n    }\n})","handlingStrategy":"fallback","validationCode":"// no pre-call validation possible; both backends fail at request time.\n// Optionally probe availability first:\n// let probe = client.get(format!(\"https://api.fxtwitter.com/status/{id}\")).send().await?;\n// if !probe.status().is_success() { warn \"post possivelmente indisponivel\" }","typeGuard":"fn is_unavailable_tweet(err: &str) -> bool {\n    err.contains(\"404\") || err.to_ascii_lowercase().contains(\"not found\")\n}","tryCatchPattern":"match unroll(id).await {\n    Err(e) => {\n        let msg = e.to_string(); // \"fxtwitter_err / graphql_err\"\n        if let Some((fx, gql)) = msg.split_once(\" / \") {\n            eprintln!(\"fxtwitter: {fx} | graphql: {gql}\");\n        }\n        if is_unavailable_tweet(&msg) {\n            eprintln!(\"post removido, privado ou inexistente\");\n        } else {\n            schedule_retry_with_backoff(id); // transient: 429/5xx/network\n        }\n    }\n    Ok(t) => t,\n}","preventionTips":["Parse both halves of the '{fx} / {graphql}' message to identify the real root cause","Retry with exponential backoff on 429/5xx; never retry 404 (deleted/protected tweet)","Monitor fxtwitter status; if the primary backend fails often, prefer the GraphQL path directly","Keep GraphQL query IDs and guest-token handling up to date with X API changes","Throttle bulk unrolls to avoid tripping rate limits on both backends at once"],"tags":["network","api","fallback","x","composite-error"],"backgroundTag":"api-request-failed","analyzedSha":"8600b91f4246848bac346874daa9e61c1fc5677a","analyzedAt":"2026-09-12T14:29:19.317Z","contentChangedAt":"2026-09-12T14:29:19.317Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}