BigPizzaV3/CodexPlusPlus · error · anyhow::Error

Page.captureScreenshot returned invalid PNG data

Error message

Page.captureScreenshot returned invalid PNG data

What it means

capture_screenshot base64-decodes the data field of the CDP Page.captureScreenshot response and then validates the PNG magic signature (89 50 4E 47 0D 0A 1A 0A). This bail fires when decoding succeeded but the bytes are not a PNG: the endpoint returned some other payload - a different image format, an HTML/text error body, or garbage - under a success result.

Source

Thrown at crates/codex-plus-core/src/bridge.rs:135

    output_path: &Path,
) -> anyhow::Result<u64> {
    let response = send_cdp_command(
        websocket_url,
        "Page.captureScreenshot",
        capture_screenshot_params(),
    )
    .await?;
    let encoded = response
        .get("result")
        .and_then(|result| result.get("data"))
        .and_then(Value::as_str)
        .filter(|data| !data.is_empty())
        .ok_or_else(|| anyhow::anyhow!("Page.captureScreenshot returned no image data"))?;
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(encoded)
        .context("failed to decode screenshot PNG")?;
    if !bytes.starts_with(&[137, 80, 78, 71, 13, 10, 26, 10]) {
        bail!("Page.captureScreenshot returned invalid PNG data");
    }
    crate::settings::atomic_write(output_path, &bytes)
        .with_context(|| format!("failed to save screenshot {}", output_path.display()))?;
    Ok(bytes.len() as u64)
}

pub async fn run_periodic_evaluations<F>(
    websocket_url: &str,
    period: Duration,
    mut next_expression: F,
) -> anyhow::Result<()>
where
    F: FnMut() -> anyhow::Result<Option<String>>,
{
    let socket = connect_cdp_websocket(websocket_url).await?;
    let mut session = CdpSession::new(socket);
    let mut interval = tokio::time::interval(period);
    loop {

View on GitHub (pinned to fb3ebd9a82)

Solutions

  1. Make sure the capture params request "format": "png" explicitly
  2. Log the first bytes of the decoded payload to identify what was actually returned
  3. Replay the same command via a raw CDP websocket or curl to inspect the raw base64
  4. If another format is intended, branch the magic-byte check per requested format

Example fix

// before
let bytes = base64::engine::general_purpose::STANDARD.decode(encoded)?;
if !bytes.starts_with(&[137, 80, 78, 71, 13, 10, 26, 10]) {
    bail!("Page.captureScreenshot returned invalid PNG data");
}

// after - request PNG explicitly and diagnose on mismatch
let params = serde_json::json!({ "format": "png" });
let bytes = base64::engine::general_purpose::STANDARD
    .decode(encoded).context("failed to decode screenshot data")?;
if !bytes.starts_with(&[137, 80, 78, 71, 13, 10, 26, 10]) {
    bail!(
        "Page.captureScreenshot returned invalid PNG data (first bytes: {:?})",
        bytes.get(..8).unwrap_or(&bytes)
    );
}
Defensive patterns

Strategy: try-catch

Type guard

fn looks_like_png(bytes: &[u8]) -> bool {
    bytes.starts_with(&[137, 80, 78, 71, 13, 10, 26, 10])
}

Try / catch

let size = match capture_screenshot(&ws_url, &params, &out).await {
    Ok(n) => n,
    Err(e) if e.to_string().contains("invalid PNG data") => {
        tracing::warn!("screenshot format unexpected: {e:#}; skipping capture");
        return Ok(0);
    }
    Err(e) => return Err(e),
};

Prevention

When it happens

Trigger: Page.captureScreenshot was invoked with a format other than png (jpeg/webp params) while validation still expects PNG magic; a non-standard Chromium/Electron build returning unusual data; a renderer crash producing a mangled payload.

Common situations: Screenshot params changed to jpeg/webp without updating the magic-byte check; Electron/Chromium version differences; capturing during a GPU process crash.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@fb3ebd9a82 (2026-08-17). Data as JSON: /api/errors/edacc26e4f9be470. Report an issue: GitHub.