tonhowtf/omniget · error · anyhow::Error

No photo URLs found in HTML

Error message

No photo URLs found in HTML

What it means

Thrown by TwitterDownloader::request_html_media when the tweet page was fetched successfully (2xx) but extract_html_photo_items found zero pbs.twimg.com/media URLs matching its regex. The HTML response simply did not contain recognizable photo media.

Solutions

  1. Confirm the tweet actually contains photos (this fallback only extracts photo URLs, not videos).
  2. Check the saved HTML (log a snippet) for pbs.twimg.com/media occurrences; if the markup changed, update the regex in extract_html_photo_items.
  3. Use the GraphQL or syndication strategies instead, or a yt-dlp fallback for video tweets.
  4. Ensure the auth cookie is set so the page is not a login wall.

Example fix

// before
let items = Self::extract_html_photo_items(&html);
if items.is_empty() {
    return Err(anyhow!("No photo URLs found in HTML"));
}
// after: include diagnostics in the error
let items = Self::extract_html_photo_items(&html);
if items.is_empty() {
    if html.contains("pbs.twimg.com") {
        return Err(anyhow!("No photo URLs found in HTML (found twimg URLs but regex missed them)"));
    }
    return Err(anyhow!("No photo URLs found in HTML (page likely login-walled or video-only)"));
}
Defensive patterns

Strategy: validation

Validate before calling

// cheap pre-check that the page actually contains photo media markup
let html = fetch_page(&url).await?;
if !html.contains("pbs.twimg.com/media") {
    eprintln!("tweet page has no photo media markup — photos-only fallback will fail");
}

Type guard

fn has_photo_markup(html: &str) -> bool {
    html.contains("pbs.twimg.com/media")
}

Try / catch

match request_html_media(&url).await {
    Err(e) if e.to_string().contains("No photo URLs found") => {
        // tweet is likely video-only or login-walled; switch strategy
        let info = ytdlp_fallback(&url).await?;
        Ok(info)
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling get_media_info on a tweet whose page HTML contains no pbs.twimg.com/media URLs: the tweet has video/GIF only (no photos), the tweet is a login wall rendering no media markup, or X changed its HTML structure so the regex no longer matches.

Common situations: Trying to download video-only tweets via the photo-oriented HTML scraper; X A/B-testing new markup; heavily JavaScript-rendered pages returning an app shell without media; login-required responses returning a 200 page with a sign-in wall.

Understand the failure class

Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.

Related errors


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

Appendix: source

Thrown at src-tauri/src/platforms/twitter/mod.rs:1053

    async fn request_html_media(&self, url: &str) -> anyhow::Result<Vec<serde_json::Value>> {
        let mut request = self
            .client
            .get(url)
            .header("User-Agent", USER_AGENT)
            .header("Accept-Language", "en")
            .header("Referer", "https://x.com/");
        if let Some(cookie) = Self::auth_cookie_string() {
            request = request.header("Cookie", cookie);
        }
        let response = request.send().await?;
        if !response.status().is_success() {
            return Err(anyhow!("HTML request returned HTTP {}", response.status()));
        }
        let html = response.text().await?;
        let items = Self::extract_html_photo_items(&html);
        if items.is_empty() {
            return Err(anyhow!("No photo URLs found in HTML"));
        }
        tracing::debug!("[twitter] html extracted {} photo entries", items.len());
        Ok(items
            .into_iter()
            .map(|item| {
                serde_json::json!({
                    "type": "photo",
                    "media_url_https": item.url,
                })
            })
            .collect())
    }

    async fn try_graphql(&self, tweet_id: &str) -> anyhow::Result<Vec<serde_json::Value>> {
        let token = self.get_guest_token(false).await?;

        match self.request_tweet(tweet_id, &token).await {
            Ok(json) => Self::extract_graphql_media(&json, tweet_id),

View on GitHub (pinned to 8600b91f42)