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 '20Reproduced 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.jsexportingshouldFetchViaGraphQL({ namespaceType, glFeatures }). app.vueuses it.paginated_dependencies_table.vueuses it, gainingglFeatureFlagsMixin. Its now-unusedisProjectNamespacecomputed andNAMESPACE_PROJECTimport 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.
-
Enable the GraphQL path for the group:
group = Group.find_by_full_path('your-group') Feature.enable(:group_dependencies_graphql, group) -
Open Secure > Dependency list on the group.
-
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 previousFull runs: paginated_dependencies_table_spec.js, dependencies_table_spec.js and app_spec.js — 90 examples, 0 failures. ESLint clean.
Related
- Parent issue: #587647 (closed)
- Group REST MR: !251901 (merged)
- Group GraphQL MR: !251903
- Flag consolidation: !251948 (merged)