Skip to content

Paginated iterator can page the wrong array and loop forever on an echoed cursor #65

Description

@giraffesyo

Pagination is detected from parameter and field names, which is conventional, but two of the guesses are unverified and both fail silently rather than loudly.

1. The iterator can page the wrong array

findItemsField (internal/analyzer/pagination.go:158) looks for a field named items, data, or results, and when it finds none it takes the first array-typed field in declaration order.

EventPage:
  type: object
  properties:
    cursor:   { type: string }
    warnings: { type: array, items: { type: string } }
    events:   { type: array, items: { $ref: "#/components/schemas/Event" } }

generates

func (c *Client) ListEventsIter(ctx context.Context, opts ...ListEventsParams) *PageIterator[string] {
	...
	return result.Warnings, next, nil
}

The iterator pages over warnings. It compiles, it runs, and it returns the wrong data. Which array wins is decided by property order in the spec, so reordering properties silently changes what the iterator yields.

A response with one array field is unambiguous. A response with several and no conventional name is not, and guessing there buys nothing: declining to generate the iterator leaves the caller with the plain operation, which already returns the whole page.

2. The iterator can loop forever

cursorFieldNames (internal/analyzer/pagination.go:16) includes bare cursor, so a response field named cursor is read as the next cursor. In a response, cursor is at least as often the echo of the request cursor.

PageIterator.Next stops only on an empty cursor:

if next == "" {
	it.done = true
}
it.nextCursor = next

A server that echoes the cursor it was given produces an endless run of identical requests from All() or ForEach, with no error and no ceiling.

The guard is worth having regardless of which field name is matched: a cursor that does not advance means the page did not advance, whatever the field is called. It also covers a server that pins its cursor on the last page rather than clearing it.

Fix

  • Use the fallback array only when the response has exactly one array field, and generate no iterator when the items field cannot be identified.
  • Stop iteration when the cursor comes back unchanged.

Keeping bare cursor in the name list is fine once the advance guard exists: APIs do use it correctly, and the guard bounds the damage when they do not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions