Draft: Keep group dependency list pagination on the GraphQL path

What does this MR do and why?

The dependency list decides whether to fetch via GraphQL or REST in two places, and the two had drifted apart.

app.vue picks the source for the initial load:

shouldFetchDependenciesViaGraphQL() {
  return this.isProjectNamespace || this.glFeatures.groupDependenciesGraphql;
}

paginated_dependencies_table.vue picked it again for keyset pagination, but only looked at the namespace:

fetchCursorPage(dispatch, cursor) {
  if (this.isProjectNamespace) {
    return dispatch('fetchDependenciesViaGraphQL', { cursor });
  }

  return dispatch('fetchDependencies', { cursor });
}

The trigger is the two expressions disagreeing, which happens for exactly one combination: a group with group_dependencies_graphql enabled. Projects take the GraphQL branch in both places and are consistent. Groups without the flag take the REST branch in both places and are also consistent. Only the group-with-flag case loads page one from GraphQL and then requests every page after it from REST.

That does not degrade gracefully — it fails. The two paths use incompatible cursors. GraphQL pagination is offset based, so its cursor is Base64("20"), or "MjA". The REST endpoint feeds params[:cursor] to Sbom::AggregationsFinder, which passes it to keyset_paginate, which expects a Base64-encoded JSON object of keyset column values. Decoding one as the other raises:

Gitlab::Pagination::Keyset::Paginator::Base64CursorConverter.parse("MjA")
# => JSON::ParserError: Empty input (after ) at line 1, column 2 ... in '20

Reproduced against the endpoint itself: GET /groups/:id/-/dependencies.json?cursor=MjA raises JSON::ParserError out of the finder, so the request 500s and the store's error handler shows an alert. A group on the GraphQL path that has more than one page of dependencies therefore cannot paginate at all.

Both call sites now go through one shouldFetchViaGraphQL helper, so there is a single expression to change if the condition moves again.

This is a pre-existing bug, not a regression from the group malware work. It surfaced while auditing whether that work needed any frontend change — it did not, the badge already renders for groups once the backend emits the field.

Out of scope here, but worth noting: the endpoint raises rather than rejecting an undecodable cursor with a 400. Hardening that is a separate change.

Who is affected

Nothing server-side is wrong, and no API consumer is affected. Sbom::AggregationsFinder rejecting a cursor it did not mint is reasonable. The defect is entirely the frontend choosing the wrong source for page two onwards.

Surface Affected Why
Group dependency list UI, group_dependencies_graphql on Yes Page one from GraphQL, pagination from REST with a GraphQL cursor
Group dependency list UI, flag off No REST for both the initial load and pagination
Project dependency list UI No GraphQL for both, in every case
GraphQL API callers of group.dependencies or group.dependencyAggregations No They mint and consume their own offset cursors
REST API and dependencies.json callers No They mint and consume their own keyset cursors

group_dependencies_graphql is off by default, so no one is hitting this today. It does block that flag's rollout for any group with more than one page of dependencies.

Changes

  • New ee/app/assets/javascripts/dependencies/utils.js exporting shouldFetchViaGraphQL({ namespaceType, glFeatures }).
  • app.vue uses it.
  • paginated_dependencies_table.vue uses it, gaining glFeatureFlagsMixin. Its now-unused isProjectNamespace computed and NAMESPACE_PROJECT import are dropped.
  • Adds the group-namespace case to the malware badge specs, which only covered projects.

Feature flag

No new flag. The behaviour is reached through the existing group_dependencies_graphql flag, which is off by default, so the fix only changes what already-flagged groups see.

The malware badge itself is gated by dependency_malware_detection, unchanged here.

How to set up and validate locally

Local testing steps

You need a group with more than one page of dependencies — the default page size is 20.

  1. Enable the GraphQL path for the group:

    group = Group.find_by_full_path('your-group')
    Feature.enable(:group_dependencies_graphql, group)
  2. Open Secure > Dependency list on the group.

  3. Open the browser network tab, filter to XHR, and click Next in the pagination control.

Before: the first load is a POST /api/graphql request. Clicking Next issues GET /groups/<path>/-/dependencies.json?cursor=... instead, carrying the GraphQL cursor. That request 500s, and the page shows an error alert rather than page two.

After: both the first load and every pagination click are POST /api/graphql, and paging works.

With group_dependencies_graphql disabled, the group list stays entirely on REST as before, including pagination, so it is unaffected. Project lists are unaffected in both cases.

Namespace group_dependencies_graphql Initial load Pagination (before) Pagination (after)
project any GraphQL GraphQL GraphQL
group on GraphQL REST — 500s on the GraphQL cursor GraphQL
group off REST REST REST

Testing

The two new pagination specs fail against the old condition and pass with the fix:

✕ dispatches fetchDependenciesViaGraphQL when clicking next
✕ dispatches fetchDependenciesViaGraphQL when clicking previous

Full runs: paginated_dependencies_table_spec.js, dependencies_table_spec.js and app_spec.js — 90 examples, 0 failures. ESLint clean.

Edited by Bala Kumar

Merge request reports

Loading
Loading