Context
#202 unifies the pagination stacks and adds the auto-closing CloseablePages view returned
by byPage(). Item-level iteration is leak-safe by construction: PageWalker.items()
eager-closes each page (copying its materialized items) before yielding, so a partial
consume never strands a connection. Page-level access instead relies on the caller wrapping
the view in use {} / try-with-resources.
Problem
CloseablePages.stream() builds its Stream without registering an onClose handler:
public fun stream(): Stream<Page<T>> =
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED),
false,
)
The backing iterator only closes a page when the consumer advances past it (next()) or the
source is exhausted (hasNext() returns false). A short-circuiting terminal —
byPage().stream().findFirst(), .limit(n), .anyMatch(...) — pulls the first page into
current and abandons the stream without advancing or exhausting, so that page is never
closed.
Because no onClose is wired, the idiomatic Java guard is a no-op too:
try (Stream<Page<X>> s = paginator.byPage().stream()) {
s.findFirst(); // Stream.close() runs no handlers → page leaks
}
The held page's live Response (body + pooled connection) is stranded. The item-level
streamAll() does not have this problem because it eager-closes.
Suggested fix
Wire the view's close() into the returned stream so try-with-resources on the Stream
releases the held page:
public fun stream(): Stream<Page<T>> =
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(iterator(), Spliterator.ORDERED),
false,
).onClose(::close)
and/or make the KDoc explicit that the CloseablePages itself — not the Stream — is the
close handle (byPage().use { it.stream()... }). Add a regression test that short-circuits a
page-level stream and asserts the held response is released.
References
Introduced in #202 (CloseablePages.stream()); part of #30.
Context
#202 unifies the pagination stacks and adds the auto-closing
CloseablePagesview returnedby
byPage(). Item-level iteration is leak-safe by construction:PageWalker.items()eager-closes each page (copying its materialized
items) before yielding, so a partialconsume never strands a connection. Page-level access instead relies on the caller wrapping
the view in
use {}/ try-with-resources.Problem
CloseablePages.stream()builds itsStreamwithout registering anonClosehandler:The backing iterator only closes a page when the consumer advances past it (
next()) or thesource is exhausted (
hasNext()returnsfalse). A short-circuiting terminal —byPage().stream().findFirst(),.limit(n),.anyMatch(...)— pulls the first page intocurrentand abandons the stream without advancing or exhausting, so that page is neverclosed.
Because no
onCloseis wired, the idiomatic Java guard is a no-op too:The held page's live
Response(body + pooled connection) is stranded. The item-levelstreamAll()does not have this problem because it eager-closes.Suggested fix
Wire the view's
close()into the returned stream so try-with-resources on theStreamreleases the held page:
and/or make the KDoc explicit that the
CloseablePagesitself — not theStream— is theclose handle (
byPage().use { it.stream()... }). Add a regression test that short-circuits apage-level stream and asserts the held response is released.
References
Introduced in #202 (
CloseablePages.stream()); part of #30.