Repository navigation
Split NuGet V2 update checks so a long module list stops dropping every update - #5577
Merged
Merged
Conversation
…ry update The V2 update path put every installed package of a source into one GetUpdates() GET request, without deduplicating ids, so a module installed side by side appeared once per version. With about 450 PowerShell 7 modules the URL grew past what PSGallery accepts (about 11.8k characters returned 200, 15.7k returned 404, 19.6k returned 414). A non-success response was only logged, so the whole source contributed zero updates and PowerShell 7 disappeared from the Software Updates source filter. BuildV2UpdateUrls now deduplicates ids case-insensitively, keeps the lowest installed version of each (compared through the manager's CompareVersions with the same VersionStringToStruct fallback KeepUpdatesNewerThanInstalled uses), and splits the query into URLs of at most 8000 characters. Keeping the lowest version gives the same server answer as listing all of them: the latest version is newer than one installed version exactly when it is newer than the lowest. KeepUpdatesNewerThanInstalled still filters against the highest installed version. Each chunk is requested on a shared HttpClient with its own error handling, so a failed chunk is logged and counted without discarding the updates the other chunks found. The query string format and the last-wins version and scope maps are unchanged. Fixes #5555 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The scoped batching change preserves existing filtering, isolates request failures, and has no identified blocking issues.
No open issues 🎉
What changed in this PR
Fixes missing updates for large installed-module lists by splitting shared NuGet V2 update requests, addressing the PowerShell 7 issue reported in #5555.
Changes:
- Deduplicates package IDs and batches update URLs to an 8,000-character target while preserving existing update filtering.
- Handles failures per request so successful chunks still contribute updates.
- Adds regression tests for oversized module lists and partial request failures.
| File | Description |
|---|---|
src/UniGetUI.PackageEngine.Tests/NuGetV2UpdatesTests.cs |
Adds local-feed tests for request batching and failure isolation. |
src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs |
Builds deduplicated V2 request batches and processes each independently. |
Review effort: Balanced
There was a problem hiding this comment.
🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉
Integration Details
{
"deliveryId": "356da1e0-ce32-11f1-948e-c5a6ce73cd42",
"headSha": "e3b0b7d3f60f6dc04da826442f0dfdf203b96680",
"reviewer": "copilot-pull-request-reviewer[bot]"
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
The V2 path of
BaseNuGet.GetAvailableUpdates_UnSafe(src/UniGetUI.PackageEngine.Managers.Generic.NuGet/BaseNuGet.cs) put every installed package for a source into one<source>/GetUpdates()?packageIds='a|b|...'&versions='...'GET request. It did not deduplicate ids, so a module installed side by side appeared once per version, and it never split the request.With about 450 PowerShell 7 modules (
Microsoft.Graph.Beta.*,Az.*), the URL grew past what PSGallery accepts. Triage measured:On a non-success response the code only logged and did
errors++, so the source contributed zero updates. PowerShell 7 then had no updates at all, which is also why it was missing from the Software Updates source filter. PowerShell 5 worked only because its module list was short enough. The same path serves PowerShell 5, Chocolatey's HTTP update path and any V2 feed.Fix
BaseNuGet.BuildV2UpdateUrls(source, installedPackages, canPrerelease). It deduplicates ids case-insensitively and keeps the lowest installed version of each, comparing through the manager'sCompareVersionswith the sameVersionStringToStructfallback thatKeepUpdatesNewerThanInstalleduses. It then splits the request intoGetUpdates()URLs of at mostMaxV2UpdateUrlLength = 8000characters. A single entry longer than that on its own still gets a URL of its own.KeepUpdatesNewerThanInstalledstill filters against the highest installed version, as before.GetAvailableUpdates_UnSafenow sends one request per chunk on a sharedHttpClient. Each chunk has its own error handling: a non-success status or an exception is logged and counted witherrors++, and the loop moves to the next chunk. A failed chunk no longer drops the updates found by the other chunks.packageIdVersionandpackageIdScopemaps (last-wins) are unchanged, so the installed version and scope shown for an update are the same as before. The query string format is unchanged too:%27quoting and the|separator encoded as%7c, now produced per item and joined.Verification
dotnet build UniGetUI.PackageEngine.Tests.csproj -p:Platform=x64 -p:UseSharedCompilation=falseexit 0 (builds BaseNuGet's project too). No new warnings in the changed files.dotnet format whitespace src --folder --verify-no-changesexit 0.dotnet format style src/UniGetUI.Windows.slnx --no-restore --verify-no-changes --include src/UniGetUI.PackageEngine.Managers.Generic.NuGet/ src/UniGetUI.PackageEngine.Tests/NuGetV2UpdatesTests.csexit 0.FullyQualifiedName~NuGetV2UpdatesTests: 2 total, 2 passed, 0 failed.BaseNuGet.csstashed, both tests failed for the reported reason ("Expected: 450 Actual: 0" and "Collection was empty"). After restoring the fix and rebuilding they passed again.Regression tests
src/UniGetUI.PackageEngine.Tests/NuGetV2UpdatesTests.cs, using a local HTTP V2 feed that rejects URLs longer than 11000 characters (mirroring the measured PSGallery behaviour):NuGetV2UpdatesTests.GetAvailableUpdatesReportsEveryModuleWhenTheInstalledListExceedsTheFeedUrlLimitNuGetV2UpdatesTests.GetAvailableUpdatesKeepsTheOtherChunksWhenOneRequestFailsBoth fail without the fix and pass with it.
Not covered
GetAvailableUpdates_UnSafestill does not incrementerrors. That is existing behaviour and is left alone.Review notes (non-blocking)
dotnet format style src/UniGetUI.Windows.slnx --no-restore --verify-no-changesexits 2 in the local worktree. All 459 diagnostics are IDE0044/IDE0060 in untouchedsrc/UniGetUI.Avalonia/ViewModels/*files, probably false positives because the CommunityToolkit source generators had not run (the Avalonia project was not built there). Neither changed file is among them, and the command scoped to the changed files exits 0. CI builds before it formats, so this should not affect the PR, but it has not been confirmed locally.GetAvailableUpdatesKeepsTheOtherChunksWhenOneRequestFailsfails because the single unchunked URL (450 ids, about 19k characters) exceeds the fake feed's 11000 limit, not because one chunk's error wiped out the others. That is still a valid regression failure for the reported bug. With the fix, the test does exercise per-chunk error isolation: the 500 hitsModule000andModule449still comes back.client.Send/ReadAsStringinside a chunk is now counted inerrorsand the loop moves on. Before, it went to the outer per-source catch, which does not incrementerrors.errorsonly feedslogger.Close(errors), so the only visible effect is the task log's error count.MaxV2UpdateUrlLength = 8000moves the ceiling rather than removing it. It sits below the ~11.8k PSGallery was measured to accept, but it has not been checked against live PSGallery or other V2 feeds such as Chocolatey community, Nexus or ProGet.Closes #5555
🤖 Generated with Claude Code