rustfs/rustfs · error · ListThroughTokenError

continuation token version {0} is not supported

Error message

continuation token version {0} is not supported

What it means

This error is thrown by `decode_continuation_token` when a client-supplied continuation token carries the merged-token envelope (`\0odm-list:` framing) but declares a token version `v` that the server does not implement (neither v1 ordinary tokens nor v2 no-progress tokens). The version number is surfaced in the message only because it is a small numeric discriminator, never token contents. It guards against tokens forged or produced by newer/older incompatible builds from being silently misinterpreted.

Source

Thrown at rustfs/src/on_demand_migration/list_through.rs:159

    pub fn encode(&self) -> String {
        // The envelope is built here from owned strings, so serialization
        // cannot fail; the fallback keeps the signature infallible.
        format!("{LIST_THROUGH_TOKEN_PREFIX}{}", serde_json::to_string(self).unwrap_or_default())
    }
}

/// What a decoded (base64-stripped) continuation token turned out to be.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ListThroughCursor {
    /// A plain local listing marker: the bucket was not merging when the token
    /// was issued, or the client is paginating a non-merged listing.
    Local(String),
    Merged(Box<ListThroughToken>),
}

#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum ListThroughTokenError {
    #[error("continuation token version {0} is not supported")]
    UnsupportedVersion(u32),
    /// The message never echoes the token: it is client-controlled input.
    #[error("continuation token is malformed")]
    Malformed,
}

/// Classifies an already base64-decoded continuation token.
///
/// Only a framed JSON object is read as a merged token;
/// anything else is a local marker, so a bucket that turns `list_through` off
/// keeps paginating with the tokens it handed out. A token that *is* an
/// envelope but was tampered with (unknown version, unknown field, truncated
/// JSON) is an error, never a silent fallback.
pub fn decode_continuation_token(decoded: &str) -> Result<ListThroughCursor, ListThroughTokenError> {
    let Some(payload) = decoded.strip_prefix(LIST_THROUGH_TOKEN_PREFIX) else {
        return Ok(ListThroughCursor::Local(decoded.to_string()));
    };
    let value = serde_json::from_str::<serde_json::Value>(payload).map_err(|_| ListThroughTokenError::Malformed)?;

View on GitHub (pinned to 5dca076efe)

Solutions

  1. Stop editing or hand-generating continuation tokens; only pass back tokens exactly as returned by the previous page's NextContinuationToken.
  2. Restart the full listing from page 1 (omit continuation-token / continuation-token parameters) to get a fresh token in a supported version.
  3. If tokens came from a newer RustFS build, upgrade this node to match, or drain in-flight paginations before downgrading.

Example fix

// before: hand-edited token from a previous page
let token = previous_token.replace("\"v\":1", "\"v\":3");
list_objects_v2(bucket, continuation_token = token); // -> UnsupportedVersion(3)

// after: echo the token unmodified
let token = previous_token;
list_objects_v2(bucket, continuation_token = token);
Defensive patterns

Strategy: try-catch

Validate before calling

// client-side (pre-flight sanity check on a token you did not just receive)
fn looks_like_supported_merged_token(decoded: &str) -> bool {
    decoded.strip_prefix("\0odm-list:")
        .and_then(|p| serde_json::from_str::<serde_json::Value>(p).ok())
        .and_then(|v| v.get("v").and_then(|v| v.as_u64()))
        .map(|v| v == 1 || v == 2)
        .unwrap_or(true) // non-merged local markers are fine
}

Type guard

fn is_supported_version(token: &serde_json::Value) -> bool {
    matches!(token.get("v").and_then(|v| v.as_u64()), Some(1) | Some(2))
}

Try / catch

match decode_continuation_token(&decoded) {
    Ok(cursor) => resume_listing(cursor),
    Err(ListThroughTokenError::UnsupportedVersion(v)) => {
        warn!(version = v, "unsupported continuation token; restarting listing");
        restart_listing_from_first_page(bucket, prefix);
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling ListObjectsV2 against a bucket with `policy.list_through` enabled and passing a continuation-token string that, once base64-decoded, starts with `\0odm-list:` and has a JSON field `"v"` set to something other than 1 or 2 (e.g. `"v":3`). Produced at list_through.rs:197 by the `Some(version) =>` fallback arm of the version match.

Common situations: A client hand-crafts or modifies a token (e.g. a test harness bumps `"v":1` to `"v":3` to probe behavior); a rolling upgrade where a newer node issued v3 tokens persisted by a client and replayed against an older node; a token copied between environments with different RustFS builds.

Related errors


AI-assisted analysis of rustfs/rustfs@5dca076efe (2026-09-06). Data as JSON: /api/errors/6fc14ff6112e4a74. Report an issue: GitHub.