{"record":{"id":"cb6a6f3620d60cda","repo":"astrid-runtime/astrid","slug":"principal-home-migration-page-exceeds-max-receipt","errorCode":null,"errorMessage":"principal-home migration page exceeds {MAX_RECEIPT_PAGE_BYTES} bytes","messagePattern":"principal-home migration page exceeds (.+?) bytes","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/principal_home_migration/receipts.rs","lineNumber":100,"sourceCode":"\n    pub(super) fn finish(mut self) -> io::Result<u64> {\n        self.flush_page()?;\n        Ok(self.page)\n    }\n\n    fn flush_page(&mut self) -> io::Result<()> {\n        if self.entries.is_empty() {\n            return Ok(());\n        }\n        let page = MigrationPage {\n            schema: ReceiptSchema::V2,\n            uid: self.uid,\n            page: self.page,\n            entries: std::mem::take(&mut self.entries),\n        };\n        let bytes = canonical_json(&page)?;\n        if bytes.len() > MAX_RECEIPT_PAGE_BYTES {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"principal-home migration page exceeds {MAX_RECEIPT_PAGE_BYTES} bytes\"),\n            ));\n        }\n        astrid_core::platform_fs::atomic_write_private_file(\n            &page_path_in(&self.directory, self.uid, self.page),\n            &bytes,\n        )?;\n        self.page = self\n            .page\n            .checked_add(1)\n            .ok_or_else(|| io::Error::other(\"migration page count overflow\"))?;\n        Ok(())\n    }\n}\n\npub(super) fn validate_receipt_pages(\n    home: &AstridHome,","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/principal_home_migration/receipts.rs#L82-L118","documentation":"Migration receipt pages are serialized as canonical JSON and each page must fit within MAX_RECEIPT_PAGE_BYTES so reads stay bounded and DoS-resistant. `PageWriter::flush_page` checks the serialized size before atomically writing the page file; if it exceeds the limit, the write is refused with this InvalidData io::Error. The limit exists because page files are later read back unconditionally and validated byte-for-byte.","triggerScenarios":"Calling `PageWriter::push` (or `finish`) when the buffered entries' canonical JSON exceeds MAX_RECEIPT_PAGE_BYTES — i.e. a single flush containing many entries or entries with very long source/destination strings such that the page is too large even though the per-page entry count (PAGE_ENTRY_LIMIT) is respected.","commonSituations":"Migrating principals with huge home directories where PAGE_ENTRY_LIMIT entries still exceed the byte budget; extremely long file paths in source/destination fields; older data formats with more verbose entry fields.","solutions":["Reduce PAGE_ENTRY_LIMIT (or split flushes) so each page's canonical JSON stays under MAX_RECEIPT_PAGE_BYTES when pushing entries.","Shorten source/destination paths in the migration inventory (e.g. migrate a shallower root) so entries serialize smaller.","Raise MAX_RECEIPT_PAGE_BYTES deliberately if the deployment can afford larger bounded reads, keeping it consistent with read-side validation in read_page.","Check for duplicate entries being pushed repeatedly (e.g. a loop bug) that inflate the page size."],"exampleFix":"// before\nwriter.push(entry)?; // many long-path entries buffer past the byte limit before flush\n// after\nif entries.len() >= PAGE_ENTRY_LIMIT / 2 { writer_flush()?; } // flush more often to keep pages under the byte cap","handlingStrategy":"try-catch","validationCode":"// estimate serialized size before flushing\nfn page_size_estimate(entries: &[MigrationEntry]) -> usize {\n    serde_json::to_vec(&MigrationPage {\n        schema: ReceiptSchema::V2,\n        uid: test_uid(),\n        page: 0,\n        entries: entries.to_vec(),\n    }).map(|b| b.len() + 1).unwrap_or(usize::MAX)\n}\nassert!(page_size_estimate(&entries) <= MAX_RECEIPT_PAGE_BYTES);","typeGuard":null,"tryCatchPattern":"match writer.finish() {\n    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"page exceeds\") => {\n        eprintln!(\"flush pages more frequently or reduce PAGE_ENTRY_LIMIT: {e}\");\n    },\n    Err(e) => return Err(e),\n    Ok(page_count) => eprintln!(\"wrote {page_count} pages\"),\n}","preventionTips":["Keep PAGE_ENTRY_LIMIT comfortably below the size that would push a page past MAX_RECEIPT_PAGE_BYTES for your typical path lengths.","Estimate entry size from source/destination path lengths and flush early when entries look large.","Warn (or fail fast) in the inventory generator when paths exceed a sane length budget.","After any change to MAX_RECEIPT_PAGE_BYTES, re-run both writer and reader validation paths."],"tags":["rust","migration","receipts","size-limit"],"backgroundTag":"payload-too-large","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}