Browse Source

Retry transient nuget.org failures instead of filing them as findings

A stalled request to nuget.org travelled out of the package run as an unhandled
exception, so the report filed it as a decompiler [EXCEPTION] - the one bucket
that has to hold nothing but real crashes - and the package was skipped without
a single type being decompiled. Seen in the 2026-08-16 sweep, where
common.logging.log4net timed out resolving its version list and decompiles
clean on a second attempt.

A 404 stays immediate: it is an answer, not a flake, and the sweep asks about
plenty of ids that are not packages.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4020/head
Siegfried Pammer 1 month ago
parent
commit
d8625ba0ef
  1. 39
      TestTools/nugetfuzz.cs

39
TestTools/nugetfuzz.cs

@ -316,6 +316,37 @@ async Task<NuGetVersion?> ResolveVersion(string id, VersionRange? range)
return versions.LastOrDefault(v => !v.IsPrerelease) ?? versions.LastOrDefault(); return versions.LastOrDefault(v => !v.IsPrerelease) ?? versions.LastOrDefault();
} }
// nuget.org stalls or resets a connection now and then. Without a retry such a flake
// travels all the way out as an unhandled exception, which the report files as a
// decompiler [EXCEPTION] - the one bucket that must contain nothing but real crashes -
// and the package is skipped without a single type being decompiled. A 404 is an
// answer, not a flake, so it is passed straight through to the caller.
async Task<T> WithRetry<T>(Func<Task<T>> request)
{
for (int attempt = 1; ; attempt++)
{
try
{
return await request();
}
catch (Exception ex) when (attempt < 3 && IsTransient(ex))
{
Console.WriteLine($" ! transient http failure ({ex.GetType().Name}), retry {attempt}/2");
await Task.Delay(TimeSpan.FromSeconds(2 * attempt));
}
}
static bool IsTransient(Exception ex) => ex switch {
// HttpClient.Timeout surfaces as a cancellation, not as a timeout
TaskCanceledException or IOException => true,
// no status code at all means the request never got an answer: DNS, reset, TLS
HttpRequestException { StatusCode: null } => true,
HttpRequestException { StatusCode: var status } =>
status == System.Net.HttpStatusCode.TooManyRequests || (int)status! >= 500,
_ => false,
};
}
async Task<List<NuGetVersion>?> GetVersions(string id) async Task<List<NuGetVersion>?> GetVersions(string id)
{ {
var key = id.ToLowerInvariant(); var key = id.ToLowerInvariant();
@ -325,8 +356,8 @@ async Task<List<NuGetVersion>?> GetVersions(string id)
return cached; return cached;
try try
{ {
var index = await http.GetFromJsonAsync<VersionIndex>( var index = await WithRetry(() => http.GetFromJsonAsync<VersionIndex>(
$"https://api.nuget.org/v3-flatcontainer/{key}/index.json"); $"https://api.nuget.org/v3-flatcontainer/{key}/index.json"));
var versions = index!.versions.Select(NuGetVersion.Parse).ToList(); var versions = index!.versions.Select(NuGetVersion.Parse).ToList();
versionCache[key] = versions; versionCache[key] = versions;
return versions; return versions;
@ -457,8 +488,8 @@ async Task<string> GetPackage(string id, NuGetVersion version)
if (!Directory.Exists(dir)) if (!Directory.Exists(dir))
{ {
Console.WriteLine($" downloading {id} {v}"); Console.WriteLine($" downloading {id} {v}");
var bytes = await http.GetByteArrayAsync( var bytes = await WithRetry(() => http.GetByteArrayAsync(
$"https://api.nuget.org/v3-flatcontainer/{idLower}/{v}/{idLower}.{v}.nupkg"); $"https://api.nuget.org/v3-flatcontainer/{idLower}/{v}/{idLower}.{v}.nupkg"));
var tmp = dir + ".tmp"; var tmp = dir + ".tmp";
if (Directory.Exists(tmp)) if (Directory.Exists(tmp))
Directory.Delete(tmp, true); Directory.Delete(tmp, true);

Loading…
Cancel
Save