Summary
When a cursor-paginated Hydra collection returns zero results (e.g. a filter excludes every row), the response's hydra:view contains only @id and @type. Both hydra:next and hydra:previous are missing, leaving clients with no way to navigate back or retry without out-of-band knowledge of the resource's cursor field and items-per-page setting.
Steps to Reproduce
-
Declare a resource with paginationViaCursor: [['field' => 'id', 'direction' => 'DESC']] and 10 rows.
-
Request a filtered range that yields no rows:
GET /so_manies?order[id]=desc&id[gt]=10
Expected Behavior
Pre-4.x behavior (asserted by the legacy Behat scenario "Cursor-based pagination with range filtered items" in features/hydra/collection.feature):
"hydra:view": {
"@id": "/so_manies?id%5Bgt%5D=10&order%5Bid%5D=desc",
"@type": "hydra:PartialCollectionView",
"hydra:previous": "/so_manies?id%5Bgt%5D=13&order%5Bid%5D=desc",
"hydra:next": "/so_manies?id%5Blt%5D=10&order%5Bid%5D=desc"
}
hydra:previous is the existing cursor filter shifted by items_per_page in the reverse direction; hydra:next is the cursor filter with the operator inverted (gt → lt).
Actual Behavior
{
"@type": "hydra:Collection",
"hydra:member": [],
"hydra:view": {
"@id": "/so_manies?id%5Bgt%5D=10&order%5Bid%5D=desc",
"@type": "hydra:PartialCollectionView"
}
}
Root Cause
src/Hydra/Serializer/PartialCollectionViewNormalizer.php, populateDataWithCursorBasedPagination(), lines 168–185:
$objects = iterator_to_array($object);
$firstObject = current($objects);
$lastObject = end($objects);
// ...
if (false !== $lastObject && \is_array($cursorPaginationAttribute)) {
$data[$hydraPrefix.'view'][$hydraPrefix.'next'] = ...;
}
if (false !== $firstObject && \is_array($cursorPaginationAttribute)) {
$data[$hydraPrefix.'view'][$hydraPrefix.'previous'] = ...;
}
When $objects is empty, current() and end() both return false, so both branches are skipped and neither key is emitted.
Suggested Fix
When $objects is empty and the request URL contains a cursor filter parameter for a field declared in paginationViaCursor, synthesize both URLs from $parsed['parameters'] directly:
hydra:next — same URL with the cursor operator inverted (gt ↔ lt), value preserved.
hydra:previous — same URL with the cursor operator preserved, value shifted by items_per_page in the cursor direction.
If no cursor filter is present in the request and the result is empty, synthesis is not possible — keep the current behavior (omit the keys).
Additional Context
Discovered while migrating features/hydra/collection.feature to functional tests on the JSON-LD refactor branch. The empty-filtered cursor scenario had to be relaxed because of this gap; non-empty scenarios still pass and assert the exact URLs from the legacy expectation.
Summary
When a cursor-paginated Hydra collection returns zero results (e.g. a filter excludes every row), the response's
hydra:viewcontains only@idand@type. Bothhydra:nextandhydra:previousare missing, leaving clients with no way to navigate back or retry without out-of-band knowledge of the resource's cursor field and items-per-page setting.Steps to Reproduce
Declare a resource with
paginationViaCursor: [['field' => 'id', 'direction' => 'DESC']]and 10 rows.Request a filtered range that yields no rows:
Expected Behavior
Pre-4.x behavior (asserted by the legacy Behat scenario "Cursor-based pagination with range filtered items" in
features/hydra/collection.feature):hydra:previousis the existing cursor filter shifted byitems_per_pagein the reverse direction;hydra:nextis the cursor filter with the operator inverted (gt→lt).Actual Behavior
{ "@type": "hydra:Collection", "hydra:member": [], "hydra:view": { "@id": "/so_manies?id%5Bgt%5D=10&order%5Bid%5D=desc", "@type": "hydra:PartialCollectionView" } }Root Cause
src/Hydra/Serializer/PartialCollectionViewNormalizer.php,populateDataWithCursorBasedPagination(), lines 168–185:When
$objectsis empty,current()andend()both returnfalse, so both branches are skipped and neither key is emitted.Suggested Fix
When
$objectsis empty and the request URL contains a cursor filter parameter for a field declared inpaginationViaCursor, synthesize both URLs from$parsed['parameters']directly:hydra:next— same URL with the cursor operator inverted (gt↔lt), value preserved.hydra:previous— same URL with the cursor operator preserved, value shifted byitems_per_pagein the cursor direction.If no cursor filter is present in the request and the result is empty, synthesis is not possible — keep the current behavior (omit the keys).
Additional Context
Discovered while migrating
features/hydra/collection.featureto functional tests on the JSON-LD refactor branch. The empty-filtered cursor scenario had to be relaxed because of this gap; non-empty scenarios still pass and assert the exact URLs from the legacy expectation.