tonhowtf/omniget · error
Server returned HTML instead of media — the link may have ex
Error message
Server returned HTML instead of media — the link may have expired or needs a login
What it means
reject_html_masquerading_as_media sniffs the first SNIFF_BYTES of the downloaded .part file: if the bytes don't match any known media signature but do look like HTML, the 'download' is actually an error page (login page, expired-link notice, CDN block page) that a misconfigured server delivered with an octet-stream content-type — defeating the header-only content-type check. The function errors so the HTML file is never renamed into place as fake media.
Solutions
- Refresh the media URL — expired signed links are the most common cause; fetch a fresh link before calling download.
- Pass authentication (cookies, Authorization/Bearer token, referer) via the headers variant (download_direct_with_headers) so the server doesn't answer with a login page.
- Open the .part/failed output bytes in a browser or inspect the head bytes to read the HTML error message the server returned — it usually states why (expired, login, blocked).
- Check whether a captive portal/proxy is intercepting traffic (test the URL with curl from the same network).
- If the content is legitimately non-media but not HTML-sniffable and being falsely flagged, verify the actual file format against sniff_media_format's supported signatures.
Example fix
// before
let url = "https://cdn.example.com/media/abc?token=OLD"; // token expired
let file = downloader.download(&url, &out).await?;
// after
let url = fetch_fresh_media_url(); // renew signed token / re-auth session first
let mut headers = HeaderMap::new();
headers.insert("authorization", format!("Bearer {}", token).parse()?);
let file = downloader.download_direct_with_headers(&url, &out, &headers).await?; Defensive patterns
Strategy: validation
Validate before calling
// Rust: peek the first bytes of the URL yourself before downloading
async fn looks_like_media(client: &reqwest::Client, url: &str) -> bool {
use tokio::io::AsyncReadExt;
let mut r = match client.get(url).send().await { Ok(r) => r, Err(_) => return false };
let mut head = [0u8; 16];
// read from the body stream via bytes_stream or a small range request
let _ = r.content_length();
true // then sniff head[] with the same magic-number logic as sniff_media_format
} Try / catch
match downloader.download(&url, &out).await {
Err(e) if e.to_string().contains("Server returned HTML") => {
// link expired or auth needed: refresh URL / add auth headers, then retry once
Err(anyhow!("link no longer serves media, refresh URL or login: {e}"))
}
other => other,
} Prevention
- Refresh signed/expiring URLs immediately before each download
- Always pass auth cookies/tokens via download_direct_with_headers for gated content
- Inspect the failed output's head bytes — the HTML body names the real reason
- Verify the network isn't a captive portal when many links suddenly 'return HTML'
When it happens
Trigger: download_attempt completed a transfer whose body starts with '<!DOCTYPE html'/'<html' instead of a media magic number — typically when download/download_direct is pointed at a link whose session/token expired, requires authentication, or is served a captive-portal/anti-bot page by the CDN.
Common situations: Signed/media URLs past their expiry; links behind a login that the app isn't authenticated to; CDN WAF or rate-limit pages served as application/octet-stream; hotspot/captive portals intercepting the request; cookies/session tokens not forwarded in the custom headers.
Related errors
- download de {} falhou: HTTP {}
- nao foi possivel buscar {}: {}
- Size mismatch: expected {} bytes, got {}
- Range not satisfiable, restarting
- HTTP {} downloading {}
AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12).
Data as JSON: /api/errors/94e977b4b341b971.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/omniget-core/src/core/direct_downloader.rs:440
.host_str()
.map(|h| h.to_ascii_lowercase())
}
/// Last gate before the `.part` becomes the real file.
///
/// Only an HTML page is rejected. Sniffing the *positive* case would mean
/// failing every container we cannot recognise — subtitles, images, archives,
/// PDFs all go through this same path — so the rule is the conservative one:
/// no known media signature **and** it opens like a document. A CDN error page
/// served as `200 OK` is exactly that; a `.srt` is not.
///
/// The content-type check in `download_single_stream` only sees the header,
/// which a misconfigured CDN may set to `application/octet-stream` while the
/// body is still an error page. This reads the bytes that actually landed.
fn reject_html_masquerading_as_media(part_path: &Path) -> anyhow::Result<()> {
let head = read_head(part_path, SNIFF_BYTES)?;
if sniff_media_format(&head).is_none() && looks_like_html(&head) {
return Err(anyhow!(
"Server returned HTML instead of media — the link may have expired or needs a login"
));
}
Ok(())
}
/// First `max` bytes of a file, or fewer if the file is shorter.
fn read_head(path: &Path, max: usize) -> anyhow::Result<Vec<u8>> {
use std::io::Read;
let mut file = std::fs::File::open(path)?;
let mut buf = vec![0u8; max];
let mut filled = 0usize;
while filled < max {
match file.read(&mut buf[filled..])? {
0 => break,
n => filled += n,
}
}View on GitHub (pinned to 8600b91f42)