From 72b074a603a413241b21a490533923f2222fc566 Mon Sep 17 00:00:00 2001 From: linkrobins Date: Fri, 19 Jun 2026 20:55:29 -0400 Subject: [PATCH] Fix 500 error when page[limit]=0 is requested `RequestUtil::extractLimit()` used a loose `! $limit` check that also matched the string "0", returning null. That null was then assigned to `OffsetPagination`'s non-nullable `int $limit`, throwing a TypeError and producing a 500 on any offset-paginated endpoint. Use a strict `=== null` check so a genuinely absent limit still returns null (the "no limit" path), while an explicit `page[limit]=0` falls through to the existing `< 1` guard and returns a 400, consistent with how negative limits are already handled. Fixes #4773 --- framework/core/src/Http/RequestUtil.php | 6 ++- .../core/tests/unit/Http/RequestUtilTest.php | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/framework/core/src/Http/RequestUtil.php b/framework/core/src/Http/RequestUtil.php index 44907717a9..e68058cf6b 100644 --- a/framework/core/src/Http/RequestUtil.php +++ b/framework/core/src/Http/RequestUtil.php @@ -155,7 +155,11 @@ public static function extractLimit(Request $request, ?int $defaultLimit = null, $limit = $defaultLimit; } - if (! $limit) { + // Only short-circuit when there is genuinely no limit (no page[limit] and no + // default). A loose `! $limit` check here also caught the string "0", returning + // null which then broke OffsetPagination's non-nullable int $limit. An explicit + // page[limit]=0 must instead fall through to the `< 1` guard below and 400. + if ($limit === null) { return null; } diff --git a/framework/core/tests/unit/Http/RequestUtilTest.php b/framework/core/tests/unit/Http/RequestUtilTest.php index c4f6a317c9..356543369d 100644 --- a/framework/core/tests/unit/Http/RequestUtilTest.php +++ b/framework/core/tests/unit/Http/RequestUtilTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Psr\Http\Message\ServerRequestInterface; +use Tobyz\JsonApiServer\Exception\BadRequestException; class RequestUtilTest extends TestCase { @@ -25,6 +26,55 @@ private function requestWithAccept(?string $accept): ServerRequestInterface return $accept === null ? $request : $request->withHeader('Accept', $accept); } + private function requestWithLimit(?string $limit): ServerRequestInterface + { + $request = new ServerRequest([], [], '/', 'GET'); + + return $request->withQueryParams($limit === null ? [] : ['page' => ['limit' => $limit]]); + } + + #[Test] + public function extract_limit_uses_an_explicitly_requested_limit(): void + { + $this->assertSame(20, RequestUtil::extractLimit($this->requestWithLimit('20'), 50, 100)); + } + + #[Test] + public function extract_limit_caps_at_the_maximum(): void + { + $this->assertSame(50, RequestUtil::extractLimit($this->requestWithLimit('100'), 20, 50)); + } + + #[Test] + public function extract_limit_falls_back_to_the_default_when_absent(): void + { + $this->assertSame(20, RequestUtil::extractLimit($this->requestWithLimit(null), 20, 50)); + } + + #[Test] + public function extract_limit_returns_null_when_there_is_no_limit_and_no_default(): void + { + $this->assertNull(RequestUtil::extractLimit($this->requestWithLimit(null), null, 50)); + } + + #[Test] + public function extract_limit_rejects_an_explicit_zero(): void + { + // Regression test: page[limit]=0 used to return null and then break + // OffsetPagination's non-nullable int $limit with a 500. It must 400 instead. + $this->expectException(BadRequestException::class); + + RequestUtil::extractLimit($this->requestWithLimit('0'), 20, 50); + } + + #[Test] + public function extract_limit_rejects_a_negative_limit(): void + { + $this->expectException(BadRequestException::class); + + RequestUtil::extractLimit($this->requestWithLimit('-5'), 20, 50); + } + public static function apiAcceptHeaders(): array { return [