rustfs/rustfs · error · RedactionError

Redaction refused the document: its node count exceeds the f

Error message

Redaction refused the document: its node count exceeds the frozen budget of 4096.

What it means

Redaction counts every map entry, array element, and scalar it visits, and refuses the document once the count passes 4096 (MAX_NODES, redaction.rs:29). The count is taken after unregistered top-level fields are dropped, so only content under allowed fields matters. The budget bounds redaction work deterministically under the frozen D05 contract.

Source

Thrown at rustfs/src/connect/offline/redaction.rs:170

#[serde(rename_all = "camelCase")]
pub struct RedactionResult {
    pub document: Map<String, Value>,
    pub canonical_json: String,
    pub redaction_version: &'static str,
    pub ruleset_hash: &'static str,
    pub redacted_count: usize,
    pub counts: RedactionCounts,
}

#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)]
pub enum RedactionError {
    #[error("Redaction refused the document: it names no registered collection surface.")]
    UnknownSurface,
    #[error("Redaction refused the document: its size in bytes exceeds the frozen budget of 262144.")]
    InputTooLarge,
    #[error("Redaction refused the document: its nesting depth exceeds the frozen budget of 8.")]
    TooDeep,
    #[error("Redaction refused the document: its node count exceeds the frozen budget of 4096.")]
    TooManyNodes,
    #[error("Redaction refused the document: it is not representable as JSON.")]
    NotRepresentable,
}

pub(super) fn redact(source: RedactionSource, document: &Map<String, Value>) -> Result<RedactionResult, RedactionError> {
    let encoded = serde_json::to_vec(document).map_err(|_| RedactionError::NotRepresentable)?;
    if encoded.len() > MAX_INPUT_BYTES {
        return Err(RedactionError::InputTooLarge);
    }

    let mut counts = RedactionCounts::default();
    let allowed = document
        .iter()
        .filter_map(|(key, value)| {
            if source.allows(key) {
                Some((key.clone(), value.clone()))
            } else {

View on GitHub (pinned to 201c653dcd)

Solutions

  1. Aggregate at the producer: send coarse counts and summaries instead of one node per drive or interface.
  2. Cap list lengths (top-N entries) before building the document.
  3. If the surface genuinely needs the detail, chunk it into several documents, each under 4096 nodes.

Example fix

// before
doc.insert("filesystemSummary", json!(per_mount_details)); // 4096+ nodes -> TooManyNodes

// after
doc.insert("filesystemSummary", json!(summarize_filesystems(&per_mount_details))); // aggregated, small
Defensive patterns

Strategy: validation

Validate before calling

fn count_nodes(value: &serde_json::Value) -> usize {
    match value {
        serde_json::Value::Object(m) => 1 + m.values().map(count_nodes).sum::<usize>(),
        serde_json::Value::Array(a) => 1 + a.iter().map(count_nodes).sum::<usize>(),
        _ => 1,
    }
}
// submit only when the allowed fields' subtrees total at most 4096 nodes

Try / catch

match redact(source, &document) {
    Err(RedactionError::TooManyNodes) => { /* aggregate or chunk; the same payload will always fail */ }
    other => other,
}

Prevention

When it happens

Trigger: redact walking an allowed field whose value tree exceeds 4096 nodes in total - for example a filesystemSummary or networkSummary holding hundreds of entries each serialized as an object with many keys (count_node at redaction.rs:230 trips).

Common situations: Large clusters emitting full per-drive or per-interface detail; producers echoing raw df, mount, or netstat tables into the document; a version change that starts including per-node breakdowns.

Related errors


AI-assisted analysis of rustfs/rustfs@201c653dcd (2026-08-23). Data as JSON: /api/errors/677ce89f4f7a47e7. Report an issue: GitHub.