Skip to content

[Hydra] Empty cursor-paginated collection omits hydra:next and hydra:previous #7953

@soyuka

Description

@soyuka

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

  1. Declare a resource with paginationViaCursor: [['field' => 'id', 'direction' => 'DESC']] and 10 rows.

  2. 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 (gtlt).

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 (gtlt), 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions