astrid-runtime/astrid · error

invalid principal-home migration receipt: {error}

Error message

invalid principal-home migration receipt: {error}

What it means

Thrown when a serialized principal-home migration receipt file fails to deserialize into a MigrationReceipt via serde_json. The library wraps the serde error in an io::Error with InvalidData kind because migration state files must be valid, schema-conformant JSON to be trusted. A corrupt or tampered receipt is treated as unusable migration state rather than being partially parsed.

Source

Thrown at crates/astrid-kernel/src/principal_home_migration/receipts.rs:171

    match fs::symlink_metadata(path) {
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(error) => Err(error),
        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
            Err(invalid_source(path, "receipt is not a regular file"))
        },
        Ok(_) => {
            astrid_core::platform_fs::validate_private_file(path)?;
            let bytes = fs::read(path)?;
            if bytes.len() > MAX_RECEIPT_INDEX_BYTES {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "principal-home migration receipt exceeds {MAX_RECEIPT_INDEX_BYTES} bytes"
                    ),
                ));
            }
            let receipt: MigrationReceipt = serde_json::from_slice(&bytes).map_err(|error| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("invalid principal-home migration receipt: {error}"),
                )
            })?;
            if receipt.page_count.get() > receipt.entry_count.get()
                || (receipt.entry_count == EntryCount::ZERO
                    && receipt.page_count != PageCount::ZERO)
            {
                return Err(invalid_source(path, "receipt page count is not canonical"));
            }
            let canonical = canonical_json(&receipt)?;
            if bytes != canonical {
                return Err(invalid_source(path, "receipt is not canonical JSON"));
            }
            Ok(Some(receipt))
        },
    }
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the receipt file at the failing path with a JSON validator (jq) to read the underlying serde error and fix or remove the malformed file.
  2. Delete the invalid receipt so migration re-runs and writes a fresh, valid receipt.
  3. If caused by a version upgrade, align the running binary with the schema version that wrote the receipt, or re-run migration from pre-migration source state.
  4. Ensure writes go through write_receipt (atomic, canonical JSON) rather than hand-editing receipt files.

Example fix

// before
let raw = std::fs::read_to_string("receipt.json")?;
// after
let bytes = std::fs::read("receipt.json")?;
let receipt: MigrationReceipt = serde_json::from_slice(&bytes)?;
write_receipt(path, &receipt)?; // canonical, size-checked, atomic
Defensive patterns

Strategy: validation

Validate before calling

fn receipt_is_parseable(bytes: &[u8]) -> bool {
    serde_json::from_slice::<serde_json::Value>(bytes).is_ok()
        && serde_json::from_slice::<MigrationReceipt>(bytes).is_ok()
}

Try / catch

match read_receipt(path) {
    Ok(r) => use(r),
    Err(e) if e.kind() == io::ErrorKind::InvalidData => {
        // quarantine/delete the corrupt receipt and re-run migration
        let _ = fs::remove_file(path);
        reinitialize_migration()?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: read_receipt is called (directly or via verify_migrated_legacy_principal_sources_retired, retire_one_receipted_source, or migrate_one_principal) and the receipt file contains malformed JSON, wrong field types, missing required fields, or values that violate MigrationReceipt's serde constraints.

Common situations: Disk corruption or a truncated write from a previous crash; manual editing of migration state files; a schema change in MigrationReceipt (renamed/removed fields) leaving stale receipts from an older version; copying receipt files between homes with different code versions.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/00df14d4c379a971. Report an issue: GitHub.