Devolutions/UniGetUI · error · InvalidDataException

PyPI simple index returned 0 packages — response may be malf

Error message

PyPI simple index returned 0 packages — response may be malformed

What it means

ParseSimpleIndexProjectNames throws InvalidDataException when the parsed 'projects' array yields zero package names. The JSON was valid and contained a 'projects' node, but every entry lacked a usable 'name' — a signal the PyPI response is malformed or the schema changed.

Source

Thrown at src/UniGetUI.PackageEngine.Managers.Pip/Pip.cs:234

            catch (Exception e)
            {
                logger.Error($"Could not write PyPI index file cache to {cacheFile}: {e.Message}");
            }

            return names;
        }

        internal static string[] ParseSimpleIndexProjectNames(string json)
        {
            var projects = (JsonNode.Parse(json) as JsonObject)?["projects"] as JsonArray;
            string[] names = projects?
                .Select(p => p?["name"]?.GetValue<string>())
                .Where(n => !string.IsNullOrEmpty(n))
                .Select(n => n!)
                .ToArray() ?? [];

            if (names.Length == 0)
                throw new InvalidDataException("PyPI simple index returned 0 packages — response may be malformed");

            return names;
        }

        internal static string[] SelectSearchMatches(string query, IEnumerable<string> allNames)
        {
            string queryLower = query.ToLowerInvariant();
            return allNames
                .Where(n => n.Contains(queryLower, StringComparison.OrdinalIgnoreCase))
                .OrderBy(n => n.StartsWith(queryLower, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
                .ThenBy(n => n.Length)
                .Take(MaxSearchResults)
                .ToArray();
        }

        private static async Task<string?> FetchLatestVersionAsync(string packageName)
        {
            await _versionFetchSemaphore.WaitAsync().ConfigureAwait(false);

View on GitHub (pinned to 9b1d7d0eab)

Solutions

  1. Delete the cached index file and re-download to rule out a corrupt local copy.
  2. Log the raw JSON length/preview when this fires to detect schema drift.
  3. Retry once; if it persists, report 'PyPI search temporarily unavailable' rather than caching an empty list.

Example fix

// before
if (names.Length == 0)
    throw new InvalidDataException("PyPI simple index returned 0 packages — response may be malformed");
// after: keep last good cache
if (names.Length == 0)
{
    Logger.Warn("PyPI index parse yielded 0 packages; keeping previous cache");
    return _cachedNames ?? throw new InvalidDataException("...");
}
Defensive patterns

Strategy: fallback

Validate before calling

// Validate the parsed JSON shape before trusting it.
var node = JsonNode.Parse(json);
var projects = (node as JsonObject)?["projects"] as JsonArray;
if (projects is null || projects.Count == 0)
    return _cachedNames ?? throw new InvalidDataException("PyPI index empty/malformed");

Type guard

static bool LooksLikeValidIndex(JsonNode? n) =>
    (n as JsonObject)?["projects"] is JsonArray arr && arr.Count > 0;

Try / catch

try { names = Pip.ParseSimpleIndexProjectNames(json); }
catch (InvalidDataException ex)
{
    Logger.Warn($"PyPI index malformed; keeping prior cache. {ex.Message}");
    names = _cachedNames ?? throw;
}

Prevention

When it happens

Trigger: PyPI returns a JSON whose 'projects' array is empty, or whose elements all have null/empty 'name' fields; a schema change in the v1+json simple index.

Common situations: Truncated/corrupt download; intermediary proxy returning an HTML error page parsed as empty projects; crates.io-style schema drift.

Understand the failure class

Related errors


AI-assisted analysis of Devolutions/UniGetUI@9b1d7d0eab (2026-08-13). Data as JSON: /api/errors/4330e6f50ad01280. Report an issue: GitHub.