From cd619c3cc407c00e19a26d479e389d1e6f7f76a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20R=C3=BCtter?= Date: Fri, 10 Jul 2026 23:20:51 +0200 Subject: [PATCH] Fix HTTP 500 on out-of-range front-end pagination page number A page number large enough to overflow the offset arithmetic in Query\Builder::forPage() silently promotes the offset to a float. offset() preserved that float, so the Stache driver's array_slice() call threw a TypeError instead of returning an empty page. Clamp the overflow in offset() itself, since forPage(), chunk(), and the Stache limitKeys() read all route through it. Also hardened OrderedQueryBuilder's own offset() override, which had the same unclamped max(0, $value) shape. Fixes #14979 Co-authored-by: Claude Opus 4.8 --- src/Query/Builder.php | 11 ++++++++++- src/Query/OrderedQueryBuilder.php | 10 +++++++++- tests/Data/Entries/EntryQueryBuilderTest.php | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 6d84d98abdf..41794417e77 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -72,7 +72,16 @@ public function limit($value) public function offset($value) { - $this->offset = max(0, $value); + $value = max(0, $value); + + // A large page number can overflow the offset arithmetic in forPage()/chunk() into a + // float, which array_slice() rejects. Clamp an out-of-range offset to PHP_INT_MAX so it + // yields an empty page instead of throwing. + if (is_float($value)) { + $value = $value >= PHP_INT_MAX ? PHP_INT_MAX : (int) $value; + } + + $this->offset = $value; return $this; } diff --git a/src/Query/OrderedQueryBuilder.php b/src/Query/OrderedQueryBuilder.php index 0beb43d6679..10d622114aa 100644 --- a/src/Query/OrderedQueryBuilder.php +++ b/src/Query/OrderedQueryBuilder.php @@ -87,7 +87,15 @@ public function limit($value) public function offset($value) { - $this->offset = max(0, $value); + $value = max(0, $value); + + // Mirrors the overflow guard in Query\Builder::offset() - an out-of-range offset + // shouldn't reach Collection::skip() as a float. + if (is_float($value)) { + $value = $value >= PHP_INT_MAX ? PHP_INT_MAX : (int) $value; + } + + $this->offset = $value; return $this; } diff --git a/tests/Data/Entries/EntryQueryBuilderTest.php b/tests/Data/Entries/EntryQueryBuilderTest.php index 014f68545c1..35e34b3a915 100644 --- a/tests/Data/Entries/EntryQueryBuilderTest.php +++ b/tests/Data/Entries/EntryQueryBuilderTest.php @@ -859,6 +859,18 @@ public function entries_are_found_using_offset() $this->assertEquals(['Post 2', 'Post 3'], $entries->map->title->all()); } + #[Test] + public function it_returns_an_empty_page_when_the_offset_overflows() + { + $this->createDummyCollectionAndEntries(); + + // A page number large enough to overflow the offset arithmetic used to crash + // array_slice() with a float; it should just yield an empty page. + $entries = Entry::query()->forPage(PHP_INT_MAX, 6)->get(); + + $this->assertCount(0, $entries); + } + #[Test] public function entries_are_found_using_where_has_when_max_items_1() {