{"record":{"id":"6814acd09c1f5642","repo":"quickwit-oss/quickwit","slug":"serializing-partialhit-should-never-fail","errorCode":null,"errorMessage":"serializing PartialHit should never fail","messagePattern":"serializing PartialHit should never fail","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-search/src/scroll_context.rs","lineNumber":231,"sourceCode":"            self.max_hits_per_page = 0;\n        }\n        self.search_after = last_hit;\n        self\n    }\n\n    pub fn scroll_key(&self) -> [u8; 16] {\n        u128::from(self.scroll_ulid).to_le_bytes()\n    }\n}\n\nimpl fmt::Display for ScrollKeyAndStartOffset {\n    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {\n        let mut payload = vec![0u8; 28];\n        payload[..16].copy_from_slice(&u128::from(self.scroll_ulid).to_le_bytes());\n        payload[16..24].copy_from_slice(&self.start_offset.to_le_bytes());\n        payload[24..28].copy_from_slice(&self.max_hits_per_page.to_le_bytes());\n        serde_json::to_writer(&mut payload, &self.search_after)\n            .expect(\"serializing PartialHit should never fail\");\n        let b64_payload = BASE64_STANDARD.encode(payload);\n        write!(formatter, \"{b64_payload}\")\n    }\n}\n\nimpl FromStr for ScrollKeyAndStartOffset {\n    type Err = &'static str;\n\n    fn from_str(scroll_id_str: &str) -> Result<Self, Self::Err> {\n        let base64_decoded: Vec<u8> = BASE64_STANDARD\n            .decode(scroll_id_str)\n            .map_err(|_| \"scroll id is invalid base64.\")?;\n        if base64_decoded.len() <= 16 + 8 + 4 {\n            return Err(\"scroll id payload is truncated\");\n        }\n        let (scroll_ulid_bytes, from_bytes, max_hits_bytes) = (\n            &base64_decoded[..16],\n            &base64_decoded[16..24],","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-search/src/scroll_context.rs#L213-L249","documentation":"ScrollKeyAndStartOffset's Display impl serializes the embedded PartialHit (search_after) into a fixed 28-byte payload buffer via serde_json::to_writer and expects success, then base64-encodes it. Since PartialHit contains only simple serializable fields (numbers/strings), serialization is infallible in practice; the expect asserts this invariant. A panic indicates a payload buffer too small for the JSON or a PartialHit containing a non-serializable type after a schema change.","triggerScenarios":"Formatting a ScrollKeyAndStartOffset (scroll key) where the JSON encoding of search_after exceeds the 28-byte buffer capacity beyond the 24 reserved bytes, or where search_after contains values serde_json cannot write (theoretically impossible with current PartialHit types).","commonSituations":"Essentially unreachable with current code; would appear after someone widens PartialHit (e.g. larger sort values, nested objects) without growing the payload buffer, surfacing as a panic when a scroll/next-page key is rendered into a search response.","solutions":["If hit, increase the payload buffer: serialize to a Vec first (serde_json::to_vec) instead of a fixed 28-byte array.","Audit recent PartialHit struct changes for new fields that grow or break JSON serialization.","Better: replace the fixed-size buffer with to_vec + extend so capacity is always sufficient."],"exampleFix":"// before\nlet mut payload = vec![0u8; 28];\n...\nserde_json::to_writer(&mut payload, &self.search_after)\n    .expect(\"serializing PartialHit should never fail\");\n// after\nlet mut payload = vec![0u8; 28];\n...\nserde_json::to_writer(&mut payload, &self.search_after)\n    .expect(\"serializing PartialHit should never fail\");\n// (if PartialHit grows, switch to)\nlet json = serde_json::to_vec(&self.search_after)\n    .expect(\"serializing PartialHit should never fail\");\npayload.extend_from_slice(&json);","handlingStrategy":"validation","validationCode":"let json = serde_json::to_vec(&search_after)?;\nassert!(json.len() <= 28, \"search_after JSON exceeds scroll key buffer\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When widening PartialHit, re-check the fixed 28-byte scroll key buffer or serialize to Vec.","Keep PartialHit fields to simple JSON-serializable types.","Add a round-trip test: Display → FromStr → equality for scroll keys."],"tags":["rust","serde","serialization","internal-invariant"],"backgroundTag":"json-marshal-failed","analyzedSha":"a39730c5cdcd1a4fe798403737ae293999ea21f8","analyzedAt":"2026-09-08T13:19:37.784Z","contentChangedAt":"2026-09-08T13:19:37.784Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}