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.
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 nameditems,data, orresults, and when it finds none it takes the first array-typed field in declaration order.generates
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 barecursor, so a response field namedcursoris read as the next cursor. In a response,cursoris at least as often the echo of the request cursor.PageIterator.Nextstops only on an empty cursor:A server that echoes the cursor it was given produces an endless run of identical requests from
All()orForEach, 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
Keeping bare
cursorin 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.