Skip to content

V2ListIterator.next() has no concurrency guard, causing duplicate page fetches on parallel calls #2730

Description

@m-zandi

Describe the bug

V2ListIterator (in src/autoPagination.ts) is missing the promiseCache
concurrency guard that V1Iterator uses to serialise parallel .next() calls.

If two pieces of code call .next() simultaneously while a page is in
flight — which can happen inside autoPagingEach under certain async
patterns — both calls independently reach turnPage(), issuing two HTTP
requests for the same next page. This produces duplicated or out-of-order
items in the result set.

V1Iterator solves this correctly:

next(): Promise<IteratorResult> {
if (this.promiseCache.currentPromise) {
return this.promiseCache.currentPromise;
}
...
}

V2ListIterator has no equivalent guard.

To Reproduce

Call .next() twice in parallel on a V2ListIterator while the first page
transition is in flight. Both calls independently invoke turnPage(),
resulting in two HTTP requests for the same page.

Expected behavior

Parallel .next() calls should be coalesced into a single in-flight promise,
exactly as V1Iterator does.

Code snippets

OS

Not environment-specific — reproduced by code inspection of src/autoPagination.ts. The missing promiseCache guard is absent regardless of operating system.

Node version

Reproducible on any supported Node version. Verified against Node 20 LTS. The bug is structural — V2ListIterator lacks the promiseCache pattern that V1Iterator implements — not a runtime behaviour difference between Node versions.

Library version

stripe-node master branch — confirmed in src/autoPagination.ts V2ListIterator class (current as of commit sha if known, otherwise master HEAD)

API version

Affects V2 API endpoints only (paths beginning with /v2/). The V2ListIterator is used exclusively for V2 list methods. API version: 2025-03-31.basil or later (any version using V2 list endpoints)

Additional context

V1Iterator (lines ~43–130) has this correct. V2ListIterator was added later
and did not carry the pattern over.

A related gap in the same class: after turnPage() returns, only one .next()
is attempted on the new iterator — so an empty intermediate page (data: []
with next_page_url set) silently terminates iteration. Both issues can be
fixed together if that's preferred.

Also happy to submit a PR once confirmed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions