{"record":{"id":"6fc14ff6112e4a74","repo":"rustfs/rustfs","slug":"continuation-token-version-0-is-not-supported","errorCode":null,"errorMessage":"continuation token version {0} is not supported","messagePattern":"continuation token version (.+?) is not supported","errorType":"error_code","errorClass":"ListThroughTokenError","httpStatus":null,"severity":"error","filePath":"rustfs/src/on_demand_migration/list_through.rs","lineNumber":159,"sourceCode":"    pub fn encode(&self) -> String {\n        // The envelope is built here from owned strings, so serialization\n        // cannot fail; the fallback keeps the signature infallible.\n        format!(\"{LIST_THROUGH_TOKEN_PREFIX}{}\", serde_json::to_string(self).unwrap_or_default())\n    }\n}\n\n/// What a decoded (base64-stripped) continuation token turned out to be.\n#[derive(Clone, Debug, PartialEq, Eq)]\npub enum ListThroughCursor {\n    /// A plain local listing marker: the bucket was not merging when the token\n    /// was issued, or the client is paginating a non-merged listing.\n    Local(String),\n    Merged(Box<ListThroughToken>),\n}\n\n#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]\npub enum ListThroughTokenError {\n    #[error(\"continuation token version {0} is not supported\")]\n    UnsupportedVersion(u32),\n    /// The message never echoes the token: it is client-controlled input.\n    #[error(\"continuation token is malformed\")]\n    Malformed,\n}\n\n/// Classifies an already base64-decoded continuation token.\n///\n/// Only a framed JSON object is read as a merged token;\n/// anything else is a local marker, so a bucket that turns `list_through` off\n/// keeps paginating with the tokens it handed out. A token that *is* an\n/// envelope but was tampered with (unknown version, unknown field, truncated\n/// JSON) is an error, never a silent fallback.\npub fn decode_continuation_token(decoded: &str) -> Result<ListThroughCursor, ListThroughTokenError> {\n    let Some(payload) = decoded.strip_prefix(LIST_THROUGH_TOKEN_PREFIX) else {\n        return Ok(ListThroughCursor::Local(decoded.to_string()));\n    };\n    let value = serde_json::from_str::<serde_json::Value>(payload).map_err(|_| ListThroughTokenError::Malformed)?;","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/rustfs/rustfs/blob/5dca076efed96e7b842de07c4c2111035ae7c7a2/rustfs/src/on_demand_migration/list_through.rs#L141-L177","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Stop editing or hand-generating continuation tokens; only pass back tokens exactly as returned by the previous page's NextContinuationToken.","Restart the full listing from page 1 (omit continuation-token / continuation-token parameters) to get a fresh token in a supported version.","If tokens came from a newer RustFS build, upgrade this node to match, or drain in-flight paginations before downgrading."],"exampleFix":"// before: hand-edited token from a previous page\nlet token = previous_token.replace(\"\\\"v\\\":1\", \"\\\"v\\\":3\");\nlist_objects_v2(bucket, continuation_token = token); // -> UnsupportedVersion(3)\n\n// after: echo the token unmodified\nlet token = previous_token;\nlist_objects_v2(bucket, continuation_token = token);","handlingStrategy":"try-catch","validationCode":"// client-side (pre-flight sanity check on a token you did not just receive)\nfn looks_like_supported_merged_token(decoded: &str) -> bool {\n    decoded.strip_prefix(\"\\0odm-list:\")\n        .and_then(|p| serde_json::from_str::<serde_json::Value>(p).ok())\n        .and_then(|v| v.get(\"v\").and_then(|v| v.as_u64()))\n        .map(|v| v == 1 || v == 2)\n        .unwrap_or(true) // non-merged local markers are fine\n}","typeGuard":"fn is_supported_version(token: &serde_json::Value) -> bool {\n    matches!(token.get(\"v\").and_then(|v| v.as_u64()), Some(1) | Some(2))\n}","tryCatchPattern":"match decode_continuation_token(&decoded) {\n    Ok(cursor) => resume_listing(cursor),\n    Err(ListThroughTokenError::UnsupportedVersion(v)) => {\n        warn!(version = v, \"unsupported continuation token; restarting listing\");\n        restart_listing_from_first_page(bucket, prefix);\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Treat continuation tokens as fully opaque: never parse, edit, or regenerate them.","Restart pagination from page 1 after any server upgrade/downgrade instead of resuming old tokens.","Do not persist tokens across deployments; re-issue listings after version changes."],"tags":["pagination","continuation-token","versioning","s3"],"backgroundTag":"unsupported-enum-value","analyzedSha":"5dca076efed96e7b842de07c4c2111035ae7c7a2","analyzedAt":"2026-09-06T05:54:05.891Z","contentChangedAt":"2026-09-06T05:54:05.891Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}