{"record":{"id":"889c3b3f2b4d493b","repo":"tonhowtf/omniget","slug":"o-servidor-respondeu-algo-que-n-o-json","errorCode":null,"errorMessage":"o servidor respondeu algo que não é JSON ({}): {}","messagePattern":"o servidor respondeu algo que não é JSON \\((.+?)\\): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/omniget-core/src/core/tools/blogs/mod.rs","lineNumber":204,"sourceCode":"                }\n                Ok(r) => return Err(anyhow!(\"HTTP {} em {}\", r.status(), url)),\n                Err(e) if attempt < TRIES => {\n                    tokio::time::sleep(wait).await;\n                    wait *= 2;\n                    let _ = e;\n                }\n                Err(e) => return Err(e.into()),\n            }\n        }\n        Err(anyhow!(\"não foi possível ler {}\", url))\n    }\n\n    /// GET com JSON de volta. Tolera o prefixo anti-sequestro do Medium.\n    pub async fn get_json(&self, url: &str) -> Result<serde_json::Value> {\n        let text = self.get_text(url).await?;\n        let body = strip_json_prefix(&text);\n        serde_json::from_str(body)\n            .map_err(|e| anyhow!(\"o servidor respondeu algo que não é JSON ({}): {}\", e, url))\n    }\n}\n\n/// O Medium serve JSON prefixado com `])}while(1);</x>` para que ninguém\n/// consiga incluir a resposta como `<script>`. É lixo antes do primeiro `{`\n/// ou `[`; cortar é obrigatório antes de parsear.\npub fn strip_json_prefix(text: &str) -> &str {\n    let t = text.trim_start();\n    if t.starts_with('{') || t.starts_with('[') {\n        return t;\n    }\n    match t.find(['{', '[']) {\n        Some(i) => &t[i..],\n        None => t,\n    }\n}\n\n// ── HTML → Markdown ────────────────────────────────────────────────────","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/tonhowtf/omniget/blob/8600b91f4246848bac346874daa9e61c1fc5677a/src-tauri/omniget-core/src/core/tools/blogs/mod.rs#L186-L222","documentation":"get_json() strips Medium's anti-hijack JSON prefix (])}while(1);</x>) then parses with serde_json. If parsing still fails, the body was not the expected JSON (HTML error page, empty body, login redirect page), and it reports the serde error plus the URL.","triggerScenarios":"A GET via get_json returns HTTP 200 but the body after strip_json_prefix is not valid JSON — e.g. serde_json::from_str fails — thrown to all callers (from_session, discover, export_publication, fetch_post, expand).","commonSituations":"Expired session causing Medium to return an HTML login/error page with 200 status; a captcha or consent page; endpoint changed its response format; truncated response body from a proxy.","solutions":["Re-capture medium.com cookies — an HTML login page usually means the session expired","Open the URL in the error in a browser to see what the server actually returns","Retry later if the site is serving a transient error page","Update parsing if Medium changed its JSON envelope; check strip_json_prefix handling"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match fetcher.get_json(url).await {\n    Err(e) if e.to_string().contains(\"não é JSON\") => {\n        // likely HTML login/error page: refresh session then retry\n        refresh_cookies();\n        return fetcher.get_json(url).await;\n    }\n    other => other,\n}","preventionTips":["Detect HTML responses (starts with '<') and treat them as auth failures","Keep strip_json_prefix tested against Medium's current prefix","Re-capture cookies when endpoints start returning HTML pages"],"tags":["json","parse-error","medium"],"backgroundTag":"invalid-json-response","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"}