Skip to content

Pagination loops terminate on size < limit even when _links.next is present, silently truncating full syncs, modified-page lists, space lists, and the deletion-reconciliation live-id set #903

Description

@laboef1900

Severity: medium · Confidence: needs verification (single finder; adversarial verifier cut off by session limit) · Area: Confluence sync

Location: backend/src/domains/confluence/services/confluence-client.ts:869

Five pagination loops use if (response.size < limit || !response._links?.next) break; — getAllSpaces (line 246), getAuditRecords (line 311), getModifiedPages (line 651), getAllPagesInSpace (line 869), getAllPageIds (line 894). Confluence Server/DC is documented to return short pages while more results remain: the requested limit "may be restricted by fixed system limits", and results removed by post-fetch permission filtering yield pages with fewer than limit items but a _links.next link (known Confluence pagination behavior; see https://jira.atlassian.com/browse/CONFSERVER-95396 and the fix for the same defect in Atlassian's own client, atlassian-api/atlassian-python-api#1616). When that happens the loop breaks early and the remainder of the result set is silently discarded. The !response._links?.next half of the condition shows the code already has the authoritative signal — the size < limit half overrides it. getAllPageIds requests limit=200, above common server caps, making clamping especially likely there. Contrast getPageVersions (line 975), which correctly relies on _links.next alone.

Failure scenario: getAllPageIds(spaceKey) gets a permission-filtered/clamped first page (e.g. 180 of 200 with next present) and stops: hundreds of live pages are missing from liveIds, so detectDeletedPages treats them all as deletion candidates — either burning up to MAX_DELETION_CONFIRMATIONS (200) extra rate-limited GETs every cycle, or (candidates > 200) permanently deferring deletion reconciliation so genuine Confluence deletions are never reflected locally. In getAllPagesInSpace the same short page means part of the space is never synced at all on a full sync.

Suggested fix: Terminate solely on the absence of _links.next (plus an empty-results guard against non-progress), and advance start by response.results.length (or follow the next link verbatim) instead of by limit.


Filed from the 2026-07-10 automated code + UX audit (17-agent static sweep + adversarial verification + live Playwright walkthrough). See tracking epic.
Tracking epic: #856

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditFinding from the automated code + UX auditbackendBackend changesbugSomething isn't workingneeds-verificationAudit finding whose adversarial verifier did not run

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions