Devolutions/UniGetUI · error · HttpRequestException

GitHub request failed with HTTP {(int)response.StatusCode} (

Error message

GitHub request failed with HTTP {(int)response.StatusCode} ({response.ReasonPhrase}): {GetErrorMessage(content)}

What it means

GitHubApiClient.SendAsync is the single chokepoint for all GitHub REST/OAuth requests. After sending the HttpRequestMessage it reads the response body and checks IsSuccessStatusCode. On any non-2xx status it throws an HttpRequestException whose message includes the numeric status code, the ReasonPhrase, and a best-effort extraction of the error body (GetErrorMessage parses the JSON for message/error/error_description, falling back to the raw body). This surfaces rate limits, auth failures, 404s, and 5xx errors uniformly.

Source

Thrown at src/UniGetUI.Interface.IpcApi/GitHubApiClient.cs:233

                    .Select(value => new KeyValuePair<string, string>(value.Key, value.Value!))
            ),
        };
        message.Headers.Accept.Clear();
        message.Headers.Accept.ParseAdd("application/json");
        return await SendAsync(message, typeInfo, cancellationToken);
    }

    private async Task<T> SendAsync<T>(
        HttpRequestMessage message,
        JsonTypeInfo<T> typeInfo,
        CancellationToken cancellationToken
    )
    {
        using HttpResponseMessage response = await _httpClient.SendAsync(message, cancellationToken);
        string content = await response.Content.ReadAsStringAsync(cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"GitHub request failed with HTTP {(int)response.StatusCode} ({response.ReasonPhrase}): {GetErrorMessage(content)}"
            );
        }

        return JsonSerializer.Deserialize(content, typeInfo)
            ?? throw new InvalidOperationException("GitHub returned an empty response.");
    }

    private static StringContent CreateJsonContent<T>(T value, JsonTypeInfo<T> typeInfo)
    {
        return new StringContent(
            JsonSerializer.Serialize(value, typeInfo),
            System.Text.Encoding.UTF8,
            "application/json"
        );
    }

    private static string GetErrorMessage(string content)

View on GitHub (pinned to 9b1d7d0eab)

Solutions

  1. Read the status code and body from the exception message to identify the specific GitHub error.
  2. If 401/403: re-authenticate via the device flow (CompleteGitHubDeviceFlowAsync) and retry.
  3. If 403 rate-limited: back off and retry after the Reset header interval; reduce request frequency.
  4. If 404 on a gist: the backup gist was deleted — call CreateCloudBackupAsync to recreate it.
  5. If 5xx: retry with exponential backoff or report a transient GitHub outage.

Example fix

// before: unguarded GitHub call
var user = await client.GetCurrentUserAsync();
// after: handle auth/rate-limit failures
try { var user = await client.GetCurrentUserAsync(); }
catch (HttpRequestException ex)
{
    if (ex.Message.Contains("401") || ex.Message.Contains("403"))
        await IpcBackupApi.CompleteGitHubDeviceFlowAsync();
    throw;
}
Defensive patterns

Strategy: try-catch

Try / catch

try { var user = await client.GetCurrentUserAsync(cancellationToken); }
catch (HttpRequestException ex)
{
    Logger.Error($"GitHub API failure: {ex.Message}");
    if (ex.Message.Contains("401") || ex.Message.Contains("403")) { /* re-authenticate */ }
    if (ex.Message.Contains("403") && ex.Message.Contains("rate limit", StringComparison.OrdinalIgnoreCase)) { /* back off */ }
}

Prevention

When it happens

Trigger: Any GitHubApiClient call (GetCurrentUserAsync, GetCurrentUserGistsAsync, GetGistAsync, CreateGistAsync, EditGistAsync, device-flow token exchange) receives a non-success HTTP status. Examples: 401 when the stored token expired or was revoked; 403 with a rate-limit body; 404 when a gist ID no longer exists; 422 when a gist request payload is invalid; 5xx for GitHub outages.

Common situations: The GitHub OAuth token expired or was revoked (401). The client hit the unauthenticated rate limit of 60 req/hour (403). A gist was manually deleted between list and edit (404). The build's UNIGETUI_GITHUB_CLIENT_ID is wrong, producing a 404 or 401 on device-flow initiation. GitHub itself is down or degraded (5xx). Network proxy intercepts and returns a non-GitHub error body.

Related errors


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