tonhowtf/omniget · error

yt-dlp returned invalid JSON

Error message

yt-dlp returned invalid JSON: {}

What it means

When yt-dlp exits successfully (status.success()), get_video_info parses its stdout as serde_json::Value. If stdout is not valid JSON (or is empty/garbage), this error is returned with the serde error. yt-dlp normally emits JSON when run with --dump-json/-j, so this indicates the command args or yt-dlp output changed.

Solutions

  1. Pin/update yt-dlp to a known-good version and ensure args still request JSON (--dump-json / -j)
  2. Check for a user yt-dlp config file injecting extra output and pass --ignore-config
  3. Capture and log the first bytes of stdout on failure to diagnose what was actually emitted
  4. Wrap parsing to include a stdout snippet in the error message for easier debugging

Example fix

// before
.map_err(|e| anyhow!("yt-dlp returned invalid JSON: {}", e))
// after
.map_err(|e| anyhow!("yt-dlp returned invalid JSON: {}; stdout head: {:?}",
    e, String::from_utf8_lossy(&result.stdout.chars().take(200).collect::<Vec<_>>())))
Defensive patterns

Strategy: validation

Validate before calling

// After a successful exit, validate stdout looks like JSON before full parse
let out = String::from_utf8_lossy(&result.stdout);
let trimmed = out.trim_start();
if !(trimmed.starts_with('{') || trimmed.starts_with('[')) {
    return Err(format!("yt-dlp output is not JSON (head: {:.120})", out));
}

Type guard

fn looks_like_json(bytes: &[u8]) -> bool {
    let s = String::from_utf8_lossy(bytes);
    let t = s.trim_start();
    t.starts_with('{') || t.starts_with('[')
}

Try / catch

match get_video_info(url).await {
    Err(e) if e.to_string().contains("invalid JSON") => {
        // check yt-dlp version, --ignore-config, and re-run with logged stdout
    }
    other => other,
}

Prevention

When it happens

Trigger: yt-dlp exits 0 but stdout contains warnings, progress text, or HTML instead of pure JSON — e.g. a wrong --dump-json flag, an old/patched yt-dlp whose output format changed, output polluted by a config file, or stdout redirected/mixed with another source.

Common situations: A user's global yt-dlp.conf injecting non-JSON output; a wrapper script printing banners; version drift where yt-dlp no longer supports the flag the app passes; locale/encoding issues corrupting output.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at src-tauri/omniget-core/src/core/ytdlp.rs:2351

            anyhow!(
                "Timeout fetching video info ({}s)",
                VIDEO_INFO_PROCESS_TIMEOUT_SECS
            )
        })?
        .map_err(|e| {
            tracing::debug!("[perf] get_video_info took {:?}", _timer_start.elapsed());
            anyhow!("Failed to run yt-dlp: {}", e)
        })?;

        tracing::debug!(
            "[perf] get_video_info: yt-dlp process exited at {:?} (attempt {})",
            _timer_start.elapsed(),
            attempt + 1
        );

        if result.status.success() {
            let json: serde_json::Value = serde_json::from_slice(&result.stdout)
                .map_err(|e| anyhow!("yt-dlp returned invalid JSON: {}", e))?;
            tracing::debug!("[perf] get_video_info took {:?}", _timer_start.elapsed());
            return Ok(json);
        }

        let stderr = String::from_utf8_lossy(&result.stderr).to_string();
        tracing::debug!(
            "[yt-dlp info] stderr ({} bytes): {}",
            stderr.len(),
            stderr.trim()
        );
        let stderr_lower = stderr.to_lowercase();
        if stderr_lower.contains("http error 429") {
            rate_limit_429_increment();
            let sanitized_url = sanitize_log_line(url);
            tracing::warn!(
                "[yt-429] rate limit in get_video_info: url={} attempt={}/{}",
                sanitized_url,
                attempt + 1,

View on GitHub (pinned to 8600b91f42)