Devolutions/UniGetUI · error · HttpRequestException

PyPI simple index returned {(int)response.StatusCode} {respo

Error message

PyPI simple index returned {(int)response.StatusCode} {response.ReasonPhrase}

What it means

Pip's index loader throws HttpRequestException when the GET to https://pypi.org/simple/ (the ~38 MB PyPI simple index, cached 24h) returns a non-success status. The status code and reason phrase are embedded in the message. This blocks pip package search until a successful download/cache.

Source

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

                string[] cached = File.ReadAllLines(cacheFile);
                if (cached.Length > 0)
                {
                    lock (_cacheLock) { _cachedNames = cached; _cacheTimestamp = File.GetLastWriteTime(cacheFile); }
                    return cached;
                }
                logger.Error("PyPI index file cache was empty, re-downloading...");
            }

            // Download fresh index
            logger.Log("Downloading PyPI simple index (one-time ~38 MB download, cached for 24 h)...");
            using HttpClient client = new(CoreTools.GenericHttpClientParameters);
            client.DefaultRequestHeaders.UserAgent.ParseAdd(CoreData.UserAgentString);
            client.DefaultRequestHeaders.Add("Accept", "application/vnd.pypi.simple.v1+json");

            using var request = new HttpRequestMessage(HttpMethod.Get, "https://pypi.org/simple/");
            using HttpResponseMessage response = client.Send(request);
            if (!response.IsSuccessStatusCode)
                throw new HttpRequestException($"PyPI simple index returned {(int)response.StatusCode} {response.ReasonPhrase}");

            string json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            string[] names = ParseSimpleIndexProjectNames(json);

            logger.Log($"Downloaded {names.Length} package names from PyPI");

            // Update memory cache before attempting file write so searches work even if file write fails
            lock (_cacheLock) { _cachedNames = names; _cacheTimestamp = DateTime.Now; }

            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)!);
                File.WriteAllLines(cacheFile, names);
            }
            catch (Exception e)
            {
                logger.Error($"Could not write PyPI index file cache to {cacheFile}: {e.Message}");
            }

View on GitHub (pinned to 9b1d7d0eab)

Solutions

  1. Retry after a short delay (the index is cached; transient failures clear on next refresh).
  2. Verify network/proxy reachability to https://pypi.org/simple/ from the host.
  3. If a stale cache file exists, the loader falls back to it only if non-empty; ensure pip proxy env (PIP_INDEX_URL/HTTP(S)_PROXY) is correct.

Example fix

// before: single shot throws on non-2xx
using HttpResponseMessage response = client.Send(request);
if (!response.IsSuccessStatusCode)
    throw new HttpRequestException($"PyPI simple index returned {(int)response.StatusCode} {response.ReasonPhrase}");
// after: one bounded retry
for (int i = 0; i < 2; i++)
{
    using var resp = client.Send(request.Clone());
    if (resp.IsSuccessStatusCode) { /* parse */ return; }
    if (i == 1) throw new HttpRequestException($"PyPI simple index returned {(int)resp.StatusCode} {resp.ReasonPhrase}");
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight reachability to pypi.org before the heavy download.
using var ping = await client.GetAsync("https://pypi.org/simple/", HttpCompletionOption.ResponseHeadersRead);
if (!ping.IsSuccessStatusCode) Logger.Warn($"PyPI not reachable: {ping.StatusCode}");

Try / catch

int attempts = 0;
HttpStatusCode? last = null;
while (attempts < 3)
{
    try { /* download + parse index */ return; }
    catch (HttpRequestException ex) { last = ParseStatus(ex.Message); attempts++; await Task.Delay(TimeSpan.FromSeconds(5 * attempts)); }
}
throw new HttpRequestException($"PyPI index unavailable after retries (last={last})");

Prevention

When it happens

Trigger: PyPI returns 4xx/5xx for the simple-index request: rate limiting (429), maintenance (5xx), DNS/proxy failure surfaced as a non-success, or a transient network blip.

Common situations: Corporate proxy/firewall blocking pypi.org; PyPI rate-limiting; transient outage; misconfigured proxy argument to pip.

Related errors


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