fix(pagination): follow meta.next so auto-pagination survives cursor mode - #319
Merged
Narayana Shanbhog Plivo (narayana-plivo) merged 2 commits intoSep 25, 2026
Merged
Conversation
…mode PlivoResourceInterface.__iter__ computed its own offsets (offset += limit) and stopped only when a page came back empty. Under the force_cursor_pagination switch a client-supplied offset is ignored and page 1 is returned instead, so that condition never fires: iteration loops forever re-yielding the same 20 records. Calls and Recordings both inherit this. __iter__ now follows meta.next, reading whichever of cursor= or offset= the query string carries, so one code path stays correct across offset, hybrid and cursor deployments without the SDK knowing which is live. Responses with no meta fall back to the offset walk. When following a cursor the walk sends no offset. Calls.list defaults offset=0, and under hybrid the server still applies a supplied offset, so offset=0 alongside a cursor would pin the walk to page 1 -- the same non-termination, on the mode QA runs today. A guard also stops the walk if the next page's params match the ones just used. Adds a cursor param to Calls.list and Recordings.list; validate_args rejected the keyword outright before this. Existing list(offset=...) calls are unaffected. Tests cover all three modes plus the meta-less fallback, mocking list() rather than hitting a server: QA is on hybrid, where offset is still applied and iteration looks healthy, so it cannot reproduce the defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jai Shankar (jaishankar-plivo)
approved these changes
Sep 25, 2026
Narayana Shanbhog Plivo (narayana-plivo)
merged commit Sep 25, 2026
af02c2a
into
master
7 of 12 checks passed
Narayana Shanbhog Plivo (narayana-plivo)
deleted the
fix/cursor-pagination
branch
September 25, 2026 11:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
PlivoResourceInterface.__iter__computed its own offsets (offset += limit)and terminated only when a page came back empty. Under
force_cursor_paginationa client-supplied offset is ignored and page 1 isreturned instead — HTTP 200, well-formed body, wrong rows — so that condition
never fires. Iteration loops forever re-yielding the same 20 records.
Callsand
Recordingsboth inherit this.Separately, neither
Calls.listnorRecordings.listaccepted acursorargument.
validate_argsrejected the keyword outright viainspect.getcallargs, beforeto_param_dictwas ever reached — so a callercould not opt out manually either.
Reproduced on live QA, not just in mocks
QA trace
us-west-1was flipped through all three deployment states(plivo/voice-consul-cfg#1411, #1412) and the SDK run against each:
force=trueenabled=truefalseIn cursor mode master issued
offset=0,20,40…240and received the identical 20rows every time. Capped at 13 requests for the test; uncapped it never returns.
Note the hybrid column: offset is still applied there, so iteration looks
perfectly healthy and QA could not have caught this before the flag was
flipped.
The fix
__iter__now followsmeta.next, reading whichever ofcursor=oroffset=the query string carries:
One code path stays correct in all three modes without the SDK knowing which is
live. Responses carrying no
metafall back to the offset walk, so resourcesoutside this rollout are unaffected.
When following a cursor, no
offsetis sent.Calls.listdefaultsoffset=0, and under hybrid the server still applies a supplied offset — sooffset=0alongside a cursor would pin the walk to page 1, reintroducing thesame non-termination on the mode QA runs today. A guard also stops the walk if
the next page's params match the ones just used, which closes the failure class
even against a server that ignores paging params entirely.
Adds
cursortoCalls.listandRecordings.listas a trailing optionalargument.
Backwards compatibility
Existing
list(offset=...)calls are untouched —offsetkeeps its0default and plain
list()still sendsoffset=0, astests/resources/test_calls.pyasserts.cursoris appended last in bothsignatures, so positional callers are unaffected.
Tests
tests/resources/test_pagination.pycovers all three modes plus the meta-lessfallback, mocking
list()rather than hitting a server — a runaway walk trips arequest cap and fails instead of hanging CI.
Full suite: 277 passed. The 4
test_jwtfailures are pre-existing on Python3.12 (
assertRaisesRegexp, removed in 3.12) and fail identically onmaster.Out of scope
Under offset pagination the Recordings walk duplicates rows on a live table —
65–73 per 240 in QA, which writes recordings continuously. That is offset
paging's inherent drift (head inserts shift the window), affects
masterandthis branch equally, and is not fixable client-side. Cursor mode eliminated it
on the same table: 0 duplicates. Worth noting as an argument for the
rollout.
🤖 Generated with Claude Code