{"record":{"id":"ca6b26f8056f5dbd","repo":"neondatabase/neon","slug":"invalid-shard-index-shard-id","errorCode":null,"errorMessage":"invalid shard index {shard_id}","messagePattern":"invalid shard index (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pageserver/client_grpc/src/client.rs","lineNumber":417,"sourceCode":"        };\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,\n        })\n    }\n}\n","sourceCodeStart":399,"sourceCodeEnd":435,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/pageserver/client_grpc/src/client.rs#L399-L435","documentation":"For a sharded ShardIndex, the shard number is 0-based and must be strictly less than the shard count. A key such as shard 4 of 4 (number >= count) is rejected here; the preceding check has already ensured the count itself matches urls.len().","triggerScenarios":"ShardSpec::new with any key where shard_number.0 >= shard_count.0 and the index is not the unsharded one. Most commonly produced by 1-based generation loops (for i in 1..=n).","commonSituations":"Generating shard indices with 1-based numbering; hand-written configs numbering shards 1..N; off-by-one after increasing the shard count.","solutions":["Use 0-based numbers: shard numbers must span 0..count, that is 0 to count-1.","Generate keys programmatically: (0..n).map(|i| ShardIndex::new(i, ShardCount::new(n))).","Check the shard_id in the message; its number must be smaller than its count."],"exampleFix":"// before: 1-based numbering -> shard 8 of 8 is invalid\nfor i in 1..=n {\n    map.insert(ShardIndex::new(i, count), url(i));\n}\n\n// after: 0-based numbering\nfor i in 0..n {\n    map.insert(ShardIndex::new(i, count), url(i));\n}","handlingStrategy":"validation","validationCode":"for shard_id in urls.keys() {\n    if !shard_id.is_unsharded() {\n        anyhow::ensure!(\n            (shard_id.shard_number.0 as usize) < urls.len(),\n            \"shard number {} out of range for {} shards\",\n            shard_id.shard_number.0,\n            urls.len()\n        );\n    }\n}","typeGuard":"fn shard_numbers_in_range(urls: &HashMap<ShardIndex, String>) -> bool {\n    urls.keys().all(|k| k.is_unsharded() || (k.shard_number.0 as usize) < urls.len())\n}","tryCatchPattern":null,"preventionTips":["Always generate shard numbers with a 0-based range (0..n), never 1..=n.","Validate numbering in one helper used by every code path that builds ShardIndex maps."],"tags":["rust","neon","pageserver","sharding","off-by-one"],"backgroundTag":"shard-index-out-of-range","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}