Skip to content

fix(rest): PaginatedIterator continues past an empty page with a next link (#3.3)#58

Merged
mjerris merged 2 commits into
mainfrom
fix/pagination-empty-page
Jul 17, 2026
Merged

fix(rest): PaginatedIterator continues past an empty page with a next link (#3.3)#58
mjerris merged 2 commits into
mainfrom
fix/pagination-empty-page

Conversation

@mjerris

@mjerris mjerris commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

The bug

PaginatedIterator._fetch_next (signalwire/signalwire/rest/_pagination.py) terminated pagination with:

if next_url and data:
    ...  # parse cursor, continue
else:
    self._done = True

The and data clause is wrong. A page can legitimately return an empty data array on THIS page while still carrying a links.next cursor (more pages exist) — e.g. a filtered page that matches nothing at the current cursor. On such a page the old condition fell into the else, set _done = True, and silently dropped every subsequent page. Any real items living beyond an empty intermediate page were never yielded.

The fix

Termination is now driven only by the absence of a next link, not by an empty data array:

# before
if next_url and data:
# after
if next_url:

An empty-but-not-last page still parses its links.next cursor and fetches the next page. No public surface change — this is a pure bugfix (SEMVER-DIFF gate confirms no version bump required).

Regression test (proven to fail on old code)

Added TestPaginatedIterator::test_empty_page_with_next_continues in tests/unit/rest/test_pagination_mock.py, driven over the shared mock_signalwire server:

  • Page 1 → {data: [], links.next: ".../addresses?cursor=page2"} (empty, but more pages)
  • Page 2 → {data: [{id: "addr-late"}], links: {}} (terminal)

It asserts the iterator yields addr-late and issued 2 GETs (the second carrying cursor=page2).

  • Against the old if next_url and data: code: FAILS — collected == [], page 2 is never fetched (iterator marked done on the empty page 1).
  • After the fix: PASSES.

Verification

PORTING_SDK=/Users/michaeljerris/src/porting-sdk bash scripts/run-ci.shCI PASS (all gates green, including TEST, TYPECHECK, LINT, FMT, REST-COVERAGE, SPEC-PARITY, PAGINATION-WIRED, SEMVER-DIFF).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PqshDQajCmDHD3xPo4CXMC

mjerris and others added 2 commits July 16, 2026 17:12
… link (#3.3)

The termination check in PaginatedIterator._fetch_next was
`if next_url and data:` — it stopped iterating as soon as a page returned
an empty `data` array, even when that same page carried a `links.next`
cursor. A page can legitimately have zero items on THIS page while more
pages exist (e.g. a filtered page that matches nothing at this cursor).
The `and data` clause marked the iterator done and silently dropped every
subsequent page.

Fix: drive termination ONLY by the absence of a next link. Change the
condition to `if next_url:` so an empty-but-not-last page still fetches
the next page via its cursor.

Regression test: test_empty_page_with_next_continues stages page 1 =
{data: [], links.next: <cursor>} and page 2 = {data: [item], links: {}}
over the shared mock, then asserts the iterator yields page 2's item and
issued 2 GETs (the second carrying the parsed cursor). This test FAILS
against the old `next_url and data` code (collects `[]`, page 2 never
fetched) and PASSES after the fix.

No public surface change (bugfix, no SEMVER bump). Full run-ci.sh green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqshDQajCmDHD3xPo4CXMC
porting-sdk #99 widens accessor_truth.py + doc_env.py to read ALL tracked
markdown (not just docs/**), surfacing real pre-existing doc drift.

DOC-ENV — correct stale env-var names to the vars the SDK actually reads:
- SWML_AUTH_USER  -> SWML_BASIC_AUTH_USER
- SWML_AUTH_PASS  -> SWML_BASIC_AUTH_PASSWORD
  (auth_mixin.py / security_config.py read SWML_BASIC_AUTH_USER/PASSWORD)
- SWML_LOG_LEVEL  -> SIGNALWIRE_LOG_LEVEL
  (logging_config.py reads SIGNALWIRE_LOG_LEVEL)
- SWML_BASIC_AUTH_PASSWORD_FILE: removed — the SDK never reads a *_FILE var;
  reworked the docker tutorial to use an env_file with the real
  SWML_BASIC_AUTH_USER/PASSWORD the SDK reads directly.

ACCESSOR-TRUTH — math skill README described a phantom `eval()`:
- the skill does NOT use Python's eval(); it parses to an AST and walks it with
  a restricted evaluator (ast.parse(mode="eval") + _safe_eval). Rewrote the
  "Safe Evaluation" section to describe the real AST-based implementation.

Not touched (flagged for human decision, deliberately NOT allowlisted):
SWML_SCHEMA_PATH / SWML_SCHEMA_MCP_DEBUG are documented in
mcp/swml-schema-search/README.md and are genuinely read by the sibling
mcp/swml-schema-search/swml_schema_mcp.py. The doc-gate flags them only because
its code perimeter is signalwire/signalwire/**/*.py (shippable SDK surface) and
excludes the standalone mcp/ tool. Docs and code are both correct; the mismatch
is a perimeter artifact, not doc drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqshDQajCmDHD3xPo4CXMC
@mjerris
mjerris merged commit 58d0d15 into main Jul 17, 2026
5 checks passed
@mjerris
mjerris deleted the fix/pagination-empty-page branch July 17, 2026 00:37
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.

1 participant