{"record":{"id":"926c01afcd737915","repo":"neondatabase/neon","slug":"invalid-shard-index-shard-id-expected-count","errorCode":null,"errorMessage":"invalid shard index {shard_id}, expected {count}","messagePattern":"invalid shard index (.+?), expected (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pageserver/client_grpc/src/client.rs","lineNumber":413,"sourceCode":"            0 => return Err(anyhow!(\"no shards provided\")),\n            1 => ShardCount::new(0), // NB: unsharded tenants use 0, like `ShardIndex::unsharded()`\n            n if n > u8::MAX as usize => return Err(anyhow!(\"too many shards: {n}\")),\n            n => ShardCount::new(n as u8),\n        };\n\n        // Validate the stripe size.\n        if stripe_size.is_none() && !count.is_unsharded() {\n            return Err(anyhow!(\"stripe size must be given for sharded tenants\"));\n        }\n        if stripe_size.is_some() && count.is_unsharded() {\n            return Err(anyhow!(\"stripe size can't be given for unsharded tenants\"));\n        }\n\n        // Validate the shard spec.\n        for (shard_id, url) in &urls {\n            // The shard index must match the computed shard count, even for unsharded tenants.\n            if shard_id.shard_count != count {\n                return Err(anyhow!(\"invalid shard index {shard_id}, expected {count}\"));\n            }\n            // The shard index' number and count must be consistent.\n            if !shard_id.is_unsharded() && shard_id.shard_number.0 >= shard_id.shard_count.0 {\n                return Err(anyhow!(\"invalid shard index {shard_id}\"));\n            }\n            // The above conditions guarantee that we have all shards 0..count: len() matches count,\n            // shard number < count, and numbers are unique (via hashmap).\n\n            // Validate the URL.\n            if PageserverProtocol::from_connstring(url)? != PageserverProtocol::Grpc {\n                return Err(anyhow!(\"invalid shard URL {url}: must use gRPC\"));\n            }\n        }\n\n        Ok(Self {\n            urls,\n            count,\n            stripe_size,","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/pageserver/client_grpc/src/client.rs#L395-L431","documentation":"Every ShardIndex key in the map must carry a shard_count equal to the number of URLs supplied (0 when there is a single, unsharded URL). A key whose count differs, for example 8 URLs each labelled shard_count=4, cannot describe a coherent shard set and is rejected. The expected count is printed in the message.","triggerScenarios":"ShardSpec::new where any key's shard_id.shard_count differs from urls.len() (with 1 mapping to 0). Typical cause: adding or removing URLs without relabelling the indices, or mixing shards from two different shard layouts.","commonSituations":"A partially applied shard split where half the URLs still use the old count; constructing ShardIndex with the default unsharded count and forgetting to set shard_count.","solutions":["Rebuild the map so every ShardIndex uses ShardCount::new(urls.len() as u8), or 0 for a single URL.","After any change to the shard set, regenerate all ShardIndex keys instead of patching individual entries.","Read the error output: it shows the offending shard_id and the expected count, so compare number and count directly."],"exampleFix":"// before: 8 URLs, but each ShardIndex still labelled with the old count of 4\nlet spec = ShardSpec::new(urls, stripe)?; // -> \"invalid shard index <4:4>, expected 8\"\n\n// after: relabel keys for the new layout\nlet count = ShardCount::new(urls.len() as u8);\nlet urls = urls.into_iter()\n    .map(|(idx, url)| (ShardIndex::new(idx.shard_number.0 % count.0, count), url))\n    .collect::<HashMap<_, _>>();\nlet spec = ShardSpec::new(urls, stripe)?;","handlingStrategy":"validation","validationCode":"// every key's shard_count must equal the map size (0 for a single URL)\nlet expected = if urls.len() == 1 { ShardCount::new(0) } else { ShardCount::new(urls.len() as u8) };\nfor shard_id in urls.keys() {\n    anyhow::ensure!(\n        shard_id.shard_count == expected,\n        \"shard index {shard_id} does not match expected count {expected}\"\n    );\n}","typeGuard":"fn shard_indices_consistent(urls: &HashMap<ShardIndex, String>) -> bool {\n    let expected = if urls.len() == 1 { ShardCount::new(0) } else { ShardCount::new(urls.len() as u8) };\n    urls.keys().all(|k| k.shard_count == expected)\n}","tryCatchPattern":null,"preventionTips":["Regenerate all ShardIndex keys whenever the shard set changes; never patch single entries.","Build the map with (0..n).map(|i| (ShardIndex::new(i, count), url(i))) so labels cannot drift."],"tags":["rust","neon","pageserver","sharding","input-validation"],"backgroundTag":"shard-config-mismatch","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}