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
- Re-extract/refresh the media URL (signed URLs expire; fetch a fresh one) and retry
- Add/refresh authentication cookies or headers so the origin stops serving a login page
- Verify the URL with a HEAD request checking Content-Type starts with the expected media type before committing to a full download
- Check for bot-protection (Cloudflare) and use appropriate headers or a resolver step
- 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
- Re-extract direct media URLs before each download — they often expire within minutes/hours
- Validate Content-Type with a HEAD request before committing to a large transfer
- Carry valid session cookies so origins don't answer with login pages
- Detect redirect chains landing on HTML pages and abort early
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
- download falhou: HTTP
- download de falhou: HTTP
- Failed to download attachment
- nao foi possivel buscar
- Failed to download aria2c: HTTP
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)