{"record":{"id":"806d156c73c0d076","repo":"tonhowtf/omniget","slug":"resposta-do-twitch-gql-sem-data","errorCode":null,"errorMessage":"resposta do Twitch GQL sem `data`","messagePattern":"resposta do Twitch GQL sem `data`","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/omniget-core/src/core/tools/twitch/gql.rs","lineNumber":181,"sourceCode":"            }\n            if !status.is_success() {\n                bail!(\"Twitch GQL respondeu HTTP {}\", status);\n            }\n            return Ok(resp.json::<Value>().await?);\n        }\n        bail!(\"Twitch GQL não respondeu depois de 5 tentativas: {}\", last)\n    }\n\n    /// Query anônima em texto (as públicas aceitam sem persisted query).\n    pub async fn query(&self, query: &str) -> anyhow::Result<Value> {\n        let body = json!({ \"query\": query });\n        let json = self.post(&body).await?;\n        if let Some(msg) = first_error(&json) {\n            bail!(\"Twitch GQL: {}\", msg);\n        }\n        json.get(\"data\")\n            .cloned()\n            .ok_or_else(|| anyhow!(\"resposta do Twitch GQL sem `data`\"))\n    }\n\n    /// Persisted query (formato em lote, como o site manda). Devolve o\n    /// primeiro elemento cru, para quem quiser ler `errors` também.\n    pub async fn persisted(&self, op: &str, hash: &str, vars: Value) -> anyhow::Result<Value> {\n        let body = json!([{\n            \"operationName\": op,\n            \"variables\": vars,\n            \"extensions\": { \"persistedQuery\": { \"version\": 1, \"sha256Hash\": hash } },\n        }]);\n        let json = self.post(&body).await?;\n        json.as_array()\n            .and_then(|a| a.first())\n            .cloned()\n            .ok_or_else(|| anyhow!(\"resposta em lote inesperada do Twitch GQL\"))\n    }\n\n    pub async fn channel(&self, login: &str) -> anyhow::Result<Channel> {","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/tools/twitch/gql.rs#L163-L199","documentation":"The GQL client's query() posts a GraphQL request and, after checking for GraphQL errors, requires a top-level 'data' key. It throws this when the response JSON lacks 'data' entirely, meaning Twitch answered but not with a usable GraphQL payload. This guards callers from nil 'data' panics downstream.","triggerScenarios":"Any channel/video/clip_video call where the Twitch GQL endpoint returns JSON without 'data': e.g. an 'errors'-shaped response that first_error missed, an authentication/introspection-style response, or Twitch serving an anti-bot/HTML-adjacent JSON error.","commonSituations":"Twitch-side incidents or schema changes; requests flagged by bot protection returning malformed JSON; deprecated/legacy API clients hitting newer endpoints; proxy or captive portal returning error JSON.","solutions":["Retry — often transient on Twitch's side","Inspect the raw response body for errors/anti-bot interstitials","Update the client (persisted query hashes / headers) if Twitch changed the API","Check network/proxy configuration that might rewrite responses"],"exampleFix":"// before\nlet data = gql.channel(\"xqc\").await?;\n// after\nlet data = match gql.channel(\"xqc\").await {\n    Ok(d) => d,\n    Err(e) if e.to_string().contains(\"sem `data`\") => {\n        tracing::warn!(\"Twitch GQL malformed response, retrying\");\n        gql.channel(\"xqc\").await?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"try-catch","validationCode":"// Not checkable pre-call; verify connectivity to gql.twitch.tv and no proxy interference.\nlet reachable = std::net::TcpStream::connect(\"gql.twitch.tv:443\").is_ok();","typeGuard":"fn has_data(json: &serde_json::Value) -> bool {\n    json.get(\"data\").map_or(false, |d| !d.is_null())\n}","tryCatchPattern":"match client.query(&q).await {\n    Err(e) if e.to_string().contains(\"sem `data`\") => {\n        // Retry once after a short delay; otherwise surface as API incident\n        tokio::time::sleep(Duration::from_secs(2)).await;\n        client.query(&q).await\n    }\n    other => other,\n}","preventionTips":["Add small backoff retries for transient Twitch incidents","Keep the client updated against Twitch GQL changes","Avoid proxies/captive portals that rewrite responses","Log the raw body when this fires to detect anti-bot interstitials"],"tags":["twitch","gql","malformed-response","graphql"],"backgroundTag":"unexpected-api-response-shape","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"}