xai-org/grok-build · error · io::Error

{}

Error message

{}

What it means

prepare_write serializes the resources state to pretty JSON before an atomic write; on serialization failure it wraps the serde_json error in io::ErrorKind::InvalidData with the message being the serde error text ("{}"). This means the in-memory snapshot could not be converted to JSON — a data-model problem, not a disk problem.

Source

Thrown at crates/codegen/xai-grok-tools/src/persistence.rs:319

            file.write_all(&json).await?;
            file.sync_all().await?;
            drop(file);
            Self::publish_durable(path, &tmp_path).await
        }
        .await;
        Self::cleanup_temp_on_error(&tmp_path, result).await
    }

    async fn cleanup_temp_on_error(tmp_path: &Path, result: io::Result<()>) -> io::Result<()> {
        if result.is_err() {
            let _ = tokio::fs::remove_file(tmp_path).await;
        }
        result
    }

    fn prepare_write(path: &Path, value: &serde_json::Value) -> io::Result<(PathBuf, Vec<u8>)> {
        let json = serde_json::to_vec_pretty(value)
            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
        Ok((path.with_extension("json.tmp"), json))
    }

    async fn replace_state_path(path: &Path, tmp_path: &Path) -> io::Result<()> {
        if path.is_dir() {
            tracing::warn!(
                "Resources state path {:?} is a directory — removing before write",
                path
            );
            tokio::fs::remove_dir_all(path).await?;
        }
        tokio::fs::rename(tmp_path, path).await
    }

    #[cfg(not(windows))]
    async fn publish_durable(path: &Path, tmp_path: &Path) -> io::Result<()> {
        // A bare filename has an empty parent, so the write would land in the server's own directory, shared by every session.
        let parent = path

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Read the wrapped serde message to identify the offending field/type in the snapshot.
  2. Sanitize non-finite floats (NaN/Infinity) to null or 0 before persisting.
  3. Add #[serde(skip_serializing_if = "Option::is_none")] / fix the custom Serialize impl that errors.
  4. Add a round-trip unit test (serialize -> deserialize) over the snapshot type to catch regressions.

Example fix

// before
metrics: { cpu: f64::NAN },  // serialization fails
// after
metrics: { cpu: if cpu.is_finite() { cpu } else { 0.0 } },
Defensive patterns

Strategy: validation

Validate before calling

fn snapshot_is_serializable(s: &ResourcesState) -> bool {
    serde_json::to_value(s).is_ok()
}

Try / catch

match persistence.save_and_flush(snapshot).await {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        tracing::error!("snapshot serialization failed: {e}");
        sanitize_and_resave(snapshot).await?;
    }
    other => other.map_err(Into::into),
}

Prevention

When it happens

Trigger: Passing a value containing unserializable content to the persistence layer — e.g. NaN/f64 non-finite floats in resource metrics, a map with non-string keys, or a type whose Serialize impl errors (skip_serializing_if misuse is not an error, but custom Serialize returning Err is).

Common situations: NaN or Infinity sneaking into measured CPU/memory stats before persisting; version drift where an enum variant's serializer was changed to error on unknown shapes; poisoned data loaded from an older schema then re-saved.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/e378c3b1d4c3e5b9. Report an issue: GitHub.