Skip to content

Fix backup() silently truncating at 100 posts - #22

Merged
jeherve merged 2 commits into
m:mainfrom
jeherve:fix/wpcom-backup-pagination
Jun 2, 2026
Merged

Fix backup() silently truncating at 100 posts#22
jeherve merged 2 commits into
m:mainfrom
jeherve:fix/wpcom-backup-pagination

Conversation

@jeherve

@jeherve jeherve commented Apr 9, 2026

Copy link
Copy Markdown
Collaborator

What this fixes

backup() currently paginates through the posts endpoint by reading meta.next_page off each response and feeding it back in as a page_handle parameter. WP.com's v1.1 posts endpoint does not return meta.next_page though — the meta object is just {"links": {...}, "wpcom": true} — so page_handle is always None and the loop exits after the first page.

Result: on any site with more than 100 posts, the backup is silently truncated to the first 100. The file reports total_posts: 100 regardless of the site's actual size, and every post beyond the first page is absent from backup['post_categories'].

I ran into this during the end-to-end test of #19 on a 766-post site. The adapter happily wrote backup-{timestamp}.json with total_posts: 100; the other 666 posts had no backup representation at all. No errors, no warnings, nothing in logs. The backup file looked completely normal until I started comparing it against my independent export of the same site.

Why this matters

Log-mode restore happens to mask this bug because it reads the change log, not the backup. That's why I didn't notice during the actual revert of #19 — the inverse replay had all the info it needed from the TSV logs.

Snapshot-mode restore is a different story. That's the fallback that runs when the change logs are missing, corrupted, or when the user explicitly asks for a full rewrite. It iterates backup['post_categories'] — so on a truncated backup, it would silently fail to restore 666 out of 766 posts on the same site without raising a single error. That's a data-loss hazard for the one code path the backup exists to enable.

What changed

One method, one block. backup() now:

  1. Uses offset-based pagination (offset=0, 100, 200, ...), which v1.1 supports reliably. Same shape everything else on WP.com v1.1 uses.
  2. Dedupes posts by ID across pages, because offset pagination can return the same post twice if a post is added between pages and shifts the underlying query.
  3. Exits on empty batch OR no-new-IDs batch — the second condition prevents an infinite loop if the query gets stuck returning the same IDs over and over.

Nothing else in backup() changes; list_categories, default category resolution, and the JSON shape are all untouched. Fix is purely in the pagination loop.

Tests

Three new tests in TestBackup plus one updated (the existing test_writes_backup_json now mocks the empty second page that the offset loop needs to exit). All 28 tests pass.

  • test_paginates_through_multiple_pages — regression: mocks a 250-post response across 3 pages and asserts all 250 end up in the backup with their IDs intact
  • test_sends_offset_not_page_handle — verifies the query uses offset=0 and does not include page_handle (so a future refactor that accidentally reverts the pagination mechanism gets caught)
  • test_dedupes_across_page_boundaries — mocks two pages returning the same three posts and asserts each is captured exactly once, and the loop terminates

Test plan

  • python3 -m unittest tests.test_wpcom_adapter passes all 28 tests
  • Verified empirically during Add ability to revert for WP.com sites #19 testing: a 766-post site generates a backup with 100 entries on main; verified locally that the fix captures all 766 (though the unit tests cover the mechanism)
  • Reviewer: sanity check the dedupe loop termination condition on a site with exactly 100 posts

Scope

Just backup(). Nothing else in the adapter, no agent markdown, no tests for anything else. Can land independently of #19 and #21 — none of them touch this code path.

That said, this one is arguably the most urgent of the adapter bugs I found during #19 testing, because a truncated backup is a silent snapshot-restore data loss waiting to happen. If #19 lands with snapshot-mode exercised, this bug becomes user-visible immediately.

Related

`backup()` paginates through the posts endpoint by reading
`meta.next_page` off each response and feeding it back in as a
`page_handle` parameter. WP.com's v1.1 posts endpoint does not
return `meta.next_page` — the meta object is just
`{"links": {...}, "wpcom": true}` — so `page_handle` is always
None and the loop exits after the first page.

Result: on any site with more than 100 posts, the backup is
silently truncated to the first 100. The backup file reports
`total_posts: 100` regardless of the site's actual size, and
every post beyond the first page is absent from
`backup['post_categories']`. Verified empirically on a 766-post
site during the PR m#19 end-to-end test: the generated backup had
100 entries.

The log-mode restore path happens to mask this because it reads
the change log, not the backup. But snapshot-mode restore (the
fallback when logs are missing or corrupted) iterates
`backup['post_categories']`, so on a truncated backup it would
silently fail to restore 666 out of 766 posts on the same site
without raising an error. That's a data-loss hazard for the one
code path the backup exists to enable.

Switches to offset-based pagination, which v1.1 supports
reliably. Adds dedupe-by-ID because offset pagination can return
the same post twice if a post is added mid-backup and shifts the
underlying query. Loop exits on an empty batch or a batch that
contributes no new IDs (the second condition prevents infinite
spin if the query is stuck).

Three new tests cover the regression, the offset mechanism, and
the dedupe behavior. Updates `test_writes_backup_json` to mock
the empty second page that the offset loop now requires to exit.
…pagination

# Conflicts:
#	tests/test_wpcom_adapter.py
@jeherve
jeherve merged commit 229885a into m:main Jun 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants