{"record":{"id":"d0fc890cf55aeb6b","repo":"tonhowtf/omniget","slug":"access-token-not-available-for-clip","errorCode":null,"errorMessage":"Access token not available for clip: {}","messagePattern":"Access token not available for clip: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src-tauri/omniget-core/src/platforms/twitch.rs","lineNumber":232,"sourceCode":"            .header(\"client-id\", CLIENT_ID)\n            .json(&body)\n            .send()\n            .await?;\n\n        if !response.status().is_success() {\n            return Err(anyhow!(\n                \"Twitch GQL token retornou HTTP {}\",\n                response.status()\n            ));\n        }\n\n        let json: serde_json::Value = response.json().await?;\n\n        let token_obj = json\n            .as_array()\n            .and_then(|arr| arr.first())\n            .and_then(|r| r.pointer(\"/data/clip/playbackAccessToken\"))\n            .ok_or_else(|| anyhow!(\"Access token not available for clip: {}\", slug))?;\n\n        let signature = token_obj\n            .get(\"signature\")\n            .and_then(|v| v.as_str())\n            .ok_or_else(|| anyhow!(\"Token sem signature\"))?\n            .to_string();\n\n        let value = token_obj\n            .get(\"value\")\n            .and_then(|v| v.as_str())\n            .ok_or_else(|| anyhow!(\"Token sem value\"))?\n            .to_string();\n\n        Ok(AccessToken { signature, value })\n    }\n\n    fn build_authenticated_url(source_url: &str, token: &AccessToken) -> String {\n        format!(","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/platforms/twitch.rs#L214-L250","documentation":"Raised by fetch_access_token when the HTTP 200 GQL response does not contain [0]./data/clip/playbackAccessToken. The response either isn't the expected batched array, the persisted query was rejected, or the clip has no playback token (e.g. deleted or restricted).","triggerScenarios":"GQL returns 200 with a persisted-query error envelope (not an array, or data.clip null), or the clip exists in metadata lookup but playback is denied so playbackAccessToken is absent/null.","commonSituations":"Stale TOKEN_HASH after a Twitch rotation producing a 200 error payload; deleted/restricted clip; schema change renaming playbackAccessToken.","solutions":["Log the full JSON body to detect persisted-query 'PersistedQueryNotFound' errors","Update TOKEN_HASH to the current hash","Fall back to yt-dlp, which obtains playback URLs independently","Retry once — token issuance can transiently fail"],"exampleFix":"// before\nlet token_obj = json.as_array().and_then(|arr| arr.first())\n    .and_then(|r| r.pointer(\"/data/clip/playbackAccessToken\"))\n    .ok_or_else(|| anyhow!(\"Access token not available for clip: {}\", slug))?;\n// after\nlet err_msg = json.pointer(\"/0/errors/0/message\").and_then(|v| v.as_str()).unwrap_or(\"missing playbackAccessToken\");\nlet token_obj = json.as_array().and_then(|arr| arr.first())\n    .and_then(|r| r.pointer(\"/data/clip/playbackAccessToken\"))\n    .ok_or_else(|| anyhow!(\"Access token unavailable for {}: {}\", slug, err_msg))?;","handlingStrategy":"fallback","validationCode":"// Detect persisted-query rejection in a 200 response body:\nlet pq_rejected = body.contains(\"PersistedQueryNotFound\");","typeGuard":"fn has_playback_token(json: &serde_json::Value) -> bool {\n    json.as_array().and_then(|a| a.first())\n        .and_then(|r| r.pointer(\"/data/clip/playbackAccessToken\"))\n        .map(|t| t.get(\"value\").and_then(|v| v.as_str()).is_some())\n        .unwrap_or(false)\n}","tryCatchPattern":"match downloader.get_media_info(url).await {\n    Err(e) if e.to_string().contains(\"Access token not available\") => fallback_ytdlp_info(url).await,\n    other => other,\n}","preventionTips":["Log the full 200 body — persisted-query errors arrive with HTTP 200","Keep TOKEN_HASH updated after Twitch rotations","Fall back to yt-dlp when token issuance fails; it does not need the GQL token"],"tags":["rust","twitch","graphql","token","response-shape"],"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"}