fix(birthdays): read only birthday fields in bounded partial multiget pages (v0.4.2) #42

Merged
jlxq0 merged 3 commits from fix/t_eb51530e-birthday-partial into main 2026-10-31 07:23:07 +00:00
Owner

Card t_eb51530e. Julian requested this directly.

Problem

Live on v0.4.1, upcoming_birthdays failed on every call with CardDAV response exceeds the configured size cap. Measured 2026-10-31 from a mounting session. The cause is that the tool fetched every card whole in one addressbook-query. Julian's Contacts book has 419 cards, single cards are about 150 KB, and the book is larger than 48 MiB: platform 8db2f43f raised the cap to 48 MiB and 929a65bd reverted it because it did not help.

Fix

  • birthday_scan first runs PROPFIND Depth: 1 to list hrefs and ETags only, the same listing list_contacts pages from.
  • It then reads the members 50 at a time with addressbook-multiget and an RFC 6352 partial address-data that names only VERSION, UID, FN, N and BDAY. No photo or other binary is transferred.
  • Stalwart honours the prop list in serialize_vcard_with_props (crates/dav/src/card/query.rs). The parser matches it by namespace (collect_address_data).
  • There is no fallback to whole cards. A page that is still over the cap fails with the cap's error. A truncated listing (507 from max-results) is refused.
  • VERSION is written only from the stored entry, so ensure_version supplies the requested version when the partial card lacks one.
  • full_name falls back to the given and family names from N when FN is missing.
  • The whole-book list_contacts client call is #[cfg(test)] now. A production path that asks for a whole book of whole cards no longer compiles.
  • Full-card reads are unchanged: list_contacts, get_contact, search_contacts and sync_contacts.

Regression evidence

  • a_book_of_photos_over_the_response_cap_still_answers builds 60 cards with 200 KB photos and uses the deployed 8 MiB default cap.
    • The old whole-book query returns TooLarge on it. The test asserts this, which reproduces the live failure.
    • The scan answers with 31 matches.
    • The fixture is over the cap both for the whole book and for one page of whole cards, so it would also fail a fix that paged without the prop list. Mutation checked: page whole cards, and add PHOTO to the properties.
  • Other tests cover:
    • missing and malformed BDAY, VALUE=text, and Apple X-APPLE-OMIT-YEAR
    • a card with no FN, and a partial card with no VERSION
    • an unparseable card, and a card the server returned that the page did not ask for
    • page boundaries (0, 1, 50 and 51 cards), a card deleted between listing and fetch, and a truncated listing
    • a window crossing the new year, and 29 February in a common year
    • a server that ignores the prop list: no fallback
    • the count summed across several books, and the mapping to the tool's answer

Review

An independent fresh-agent review covered round 1 and round 2:

  • Round 1: nothing blocking. Its four low or nit findings and three surviving mutations are fixed, and all three mutations are now caught.
  • Round 2: nothing blocking. One dedup survivor and a stale card count are fixed, and that mutation is now caught.

The reviewer confirmed the final head 613166b.

Gates

cargo fmt --check, clippy -D warnings (stable 1.99.1 and default), cargo test (251 passed), cargo audit, cargo deny and gitleaks all pass locally.

Version 0.4.2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E2xQRc9frmr6reeuvPjGdJ

Card t_eb51530e. Julian requested this directly. ## Problem Live on v0.4.1, `upcoming_birthdays` failed on every call with `CardDAV response exceeds the configured size cap`. Measured 2026-10-31 from a mounting session. The cause is that the tool fetched every card whole in one `addressbook-query`. Julian's `Contacts` book has 419 cards, single cards are about 150 KB, and the book is larger than 48 MiB: platform `8db2f43f` raised the cap to 48 MiB and `929a65bd` reverted it because it did not help. ## Fix - `birthday_scan` first runs `PROPFIND Depth: 1` to list hrefs and ETags only, the same listing `list_contacts` pages from. - It then reads the members 50 at a time with `addressbook-multiget` and an RFC 6352 partial `address-data` that names only `VERSION`, `UID`, `FN`, `N` and `BDAY`. No photo or other binary is transferred. - Stalwart honours the `prop` list in `serialize_vcard_with_props` (`crates/dav/src/card/query.rs`). The parser matches it by namespace (`collect_address_data`). - There is no fallback to whole cards. A page that is still over the cap fails with the cap's error. A truncated listing (507 from `max-results`) is refused. - `VERSION` is written only from the stored entry, so `ensure_version` supplies the requested version when the partial card lacks one. - `full_name` falls back to the given and family names from `N` when `FN` is missing. - The whole-book `list_contacts` client call is `#[cfg(test)]` now. A production path that asks for a whole book of whole cards no longer compiles. - Full-card reads are unchanged: `list_contacts`, `get_contact`, `search_contacts` and `sync_contacts`. ## Regression evidence - `a_book_of_photos_over_the_response_cap_still_answers` builds 60 cards with 200 KB photos and uses the deployed 8 MiB default cap. - The old whole-book query returns `TooLarge` on it. The test asserts this, which reproduces the live failure. - The scan answers with 31 matches. - The fixture is over the cap both for the whole book and for one page of whole cards, so it would also fail a fix that paged without the `prop` list. Mutation checked: page whole cards, and add `PHOTO` to the properties. - Other tests cover: - missing and malformed `BDAY`, `VALUE=text`, and Apple `X-APPLE-OMIT-YEAR` - a card with no `FN`, and a partial card with no `VERSION` - an unparseable card, and a card the server returned that the page did not ask for - page boundaries (0, 1, 50 and 51 cards), a card deleted between listing and fetch, and a truncated listing - a window crossing the new year, and 29 February in a common year - a server that ignores the `prop` list: no fallback - the count summed across several books, and the mapping to the tool's answer ## Review An independent fresh-agent review covered round 1 and round 2: - Round 1: nothing blocking. Its four low or nit findings and three surviving mutations are fixed, and all three mutations are now caught. - Round 2: nothing blocking. One dedup survivor and a stale card count are fixed, and that mutation is now caught. The reviewer confirmed the final head 613166b. ## Gates `cargo fmt --check`, `clippy -D warnings` (stable 1.99.1 and default), `cargo test` (251 passed), `cargo audit`, `cargo deny` and `gitleaks` all pass locally. Version 0.4.2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E2xQRc9frmr6reeuvPjGdJ
upcoming_birthdays fetched every card whole in one addressbook-query, so a
book with photos crossed the 8 MiB response cap and every call failed with
"CardDAV response exceeds the configured size cap". List the book first,
then multiget SCAN_PAGE members with an RFC 6352 partial address-data
naming VERSION, UID, FN, N and BDAY. No fallback to whole cards; a
truncated listing is refused. The whole-book client call is test-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2xQRc9frmr6reeuvPjGdJ
Extract scan_books and birthdays_result so the summed contacts_scanned
and the Found-to-answer mapping are tested; teach the fake to serve an
unparseable card and an unrequested one; read VERSION from unfolded
lines; correct two test comments; record the reverted 48 MiB cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2xQRc9frmr6reeuvPjGdJ
test(birthdays): review round 2: pin the per-page dedup; correct the Contacts card count
All checks were successful
CI / cargo (pull_request) Successful in 1m12s
CI / docker (pull_request) Successful in 1m34s
613166b2ee
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2xQRc9frmr6reeuvPjGdJ
jlxq0 merged commit b555fc48f0 into main 2026-10-31 07:23:07 +00:00
Sign in to join this conversation.
No reviewers
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jlxq0/carddav-mcp!42
No description provided.