tonhowtf/omniget · error

Threads returned HTTP

Error message

Threads returned HTTP {}

What it means

fetch_page in threads.rs checks response.status().is_success() after fetching the Threads page and converts any non-2xx into this error, embedding the HTTP status. It is a thin wrapper turning an HTTP failure from threads.com into a library error, so callers know the page fetch itself failed rather than the parsing.

Solutions

  1. Read the embedded status: 403/429 means throttle or change User-Agent; 404 means the post does not exist
  2. Back off and retry with exponential delay for 429/5xx
  3. Use a residential proxy or different egress IP if 403 persists
  4. Validate the URL points to an existing public post before calling

Example fix

// before
Err(anyhow!("Threads returned HTTP {}", response.status()))
// after
Err(anyhow!("Threads returned HTTP {}; {}", response.status(),
    if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { "rate limited, retry later" } else { "check URL/UA" }))
Defensive patterns

Strategy: retry

Try / catch

match threads.get_media_info(url).await {
    Err(e) if e.to_string().contains("Threads returned HTTP") => {
        let status = extract_status(&e);
        if matches!(status, 429 | 500..=599) { retry_with_backoff(); }
        else { surface_error("Thread unavailable (check URL)"); }
    }
    other => other?,
}

Prevention

When it happens

Trigger: fetch_post calls fetch_page (with either browser UA or Googlebot UA) and Threads responds 4xx/5xx — e.g. 403 from bot detection, 404 for an unknown post, 429 rate limiting, or 5xx server errors.

Common situations: Threads' bot protection rejecting the client's UA/TLS fingerprint (403); scraping too aggressively (429); posting a typo'd URL pointing at a nonexistent post (404); transient Threads outages (5xx).

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12). Data as JSON: /api/errors/abc032cacb8a9d51. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/omniget-core/src/platforms/threads.rs:123

    async fn fetch_page(&self, url: &str, user_agent: &str) -> anyhow::Result<String> {
        let response = self
            .client
            .get(url)
            .header("User-Agent", user_agent)
            .header(
                "Accept",
                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            )
            .header("Accept-Language", "en-GB,en;q=0.9")
            .header("Sec-Fetch-Dest", "document")
            .header("Sec-Fetch-Mode", "navigate")
            .header("Sec-Fetch-Site", "none")
            .header("Sec-Fetch-User", "?1")
            .send()
            .await?;

        if !response.status().is_success() {
            return Err(anyhow!("Threads returned HTTP {}", response.status()));
        }

        Ok(response.text().await?)
    }

    /// Cerca il post nei <script data-sjs> della pagina. Threads cambia spesso
    /// il nesting del bootstrap (require/__bbox/result/data/media), quindi invece
    /// di un path fisso parsiamo ogni script e cerchiamo ricorsivamente
    /// l'oggetto con il `code` del post e chiavi media.
    fn find_post_in_html(html: &str, post_id: &str) -> Option<serde_json::Value> {
        let re = Regex::new(r#"(?s)<script[^>]*data-sjs[^>]*>(.*?)</script>"#).ok()?;

        for cap in re.captures_iter(html) {
            let script_content = cap.get(1).map(|m| m.as_str()).unwrap_or("");

            if !script_content.contains(post_id) {
                continue;
            }

View on GitHub (pinned to 8600b91f42)