tonhowtf/omniget · error

Server returned HTML instead of media — URL may have expired

Error message

Server returned HTML instead of media — URL may have expired

What it means

After a successful HTTP status, download_single_stream inspects the Content-Type header; if the origin responds with text/html, the 'media' URL actually served a web page (login page, error page, or redirect-to-home), so the downloader refuses to write HTML bytes into the target file. This protects users from saving corrupt non-media files.

Solutions

  1. Re-extract/refresh the media URL (signed URLs expire; fetch a fresh one) and retry
  2. Add/refresh authentication cookies or headers so the origin stops serving a login page
  3. Verify the URL with a HEAD request checking Content-Type starts with the expected media type before committing to a full download
  4. Check for bot-protection (Cloudflare) and use appropriate headers or a resolver step
  5. If it persists, confirm the URL points to the actual file, not a watch/landing page

Example fix

// before: trust any 200 response
let resp = client.get(url).send().await?;
// after: pre-flight content-type check
let resp = client.get(url).send().await?;
let ct = resp.headers().get("content-type").and_then(|v| v.to_str().ok()).unwrap_or("");
if ct.contains("text/html") {
    let fresh = reextract_media_url(&source).await?;
    return download(fresh).await; // retry with a newly extracted URL
}
Defensive patterns

Strategy: validation

Validate before calling

let resp = client.head(url).send().await?;
let ct = resp.headers().get("content-type").and_then(|v| v.to_str().ok()).unwrap_or("");
if ct.contains("text/html") {
    eprintln!("URL serves HTML, refresh the media URL before downloading");
}

Type guard

fn is_media_response(resp: &reqwest::Response) -> bool {
    resp.headers().get("content-type")
        .and_then(|v| v.to_str().ok())
        .map(|ct| !ct.contains("text/html"))
        .unwrap_or(true)
}

Try / catch

match download(url, path).await {
    Err(e) if e.to_string().contains("HTML instead of media") => {
        let fresh = reextract_media_url(&source).await?;
        download(&fresh, path).await?;
    }
    r => r?,
}

Prevention

When it happens

Trigger: The remote URL returned 200 but with Content-Type containing 'text/html': typically an expired signed URL redirected to an HTML error/login page, a CDN served an interstitial/captcha page, or an extractor produced a stale or wrong media URL.

Common situations: YouTube/social-media direct URLs that expire within hours; paywalled or region-locked content where the origin answers with an HTML login page; Cloudflare/captcha challenge pages; hotlink protection redirecting to an HTML notice.

Related errors


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

Appendix: source

Thrown at src-tauri/omniget-core/src/core/direct_downloader.rs:513

    let mut offset = 0u64;
    if existing_bytes > 0 {
        if response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
            offset = existing_bytes;
        } else if response.status() == reqwest::StatusCode::RANGE_NOT_SATISFIABLE {
            let _ = std::fs::remove_file(part_path);
            return Err(anyhow!("Range not satisfiable, restarting"));
        } else if !response.status().is_success() {
            return Err(anyhow!("HTTP {} downloading {}", response.status(), url));
        }
    } else if !response.status().is_success() {
        return Err(anyhow!("HTTP {} downloading {}", response.status(), url));
    }

    if let Some(ct) = response.headers().get("content-type") {
        if let Ok(ct_str) = ct.to_str() {
            if ct_str.contains("text/html") {
                return Err(anyhow!(
                    "Server returned HTML instead of media — URL may have expired"
                ));
            }
        }
    }

    use std::io::Write;
    let raw_file = if offset > 0 {
        std::fs::OpenOptions::new().append(true).open(part_path)?
    } else {
        std::fs::File::create(part_path)?
    };

    let mut file = std::io::BufWriter::with_capacity(256 * 1024, raw_file);
    let mut downloaded = offset;
    let mut stream = response.bytes_stream();

    let mut last_emit = std::time::Instant::now();

View on GitHub (pinned to 8600b91f42)