mirror of
https://github.com/FroggMaster/CreamInstaller.git
synced 2026-06-12 11:01:23 -07:00
Improve HTTP Client Manager / Improved Debug Error Messaging
- Fixed issues with how HttpClient was being created and managed, which could lead to socket leaks and connection problems. Now the app reuses a single shared HttpClient instead of creating new ones repeatedly, and properly manages the underlying connection handler. - Improved connection handling with better timeouts, safer pooling settings, and better handling of network errors and DNS changes. - Improved Debug Error messaging for failed SteamCMD querys
This commit is contained in:
@@ -247,7 +247,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
string dlcName = null;
|
||||
string dlcIcon = null;
|
||||
bool onSteamStore = false;
|
||||
StoreAppData dlcStoreAppData = await SteamStore.QueryStoreAPI(dlcAppId, true);
|
||||
StoreAppData dlcStoreAppData = await SteamStore.QueryStoreAPI(dlcAppId, true, 0, name, appId);
|
||||
if (dlcStoreAppData is not null)
|
||||
{
|
||||
dlcName = dlcStoreAppData.Name;
|
||||
@@ -277,7 +277,7 @@ internal sealed partial class SelectForm : CustomForm
|
||||
string fullGameIcon = null;
|
||||
bool fullGameOnSteamStore = false;
|
||||
StoreAppData fullGameStoreAppData =
|
||||
await SteamStore.QueryStoreAPI(fullGameAppId, true);
|
||||
await SteamStore.QueryStoreAPI(fullGameAppId, true, 0, null, null);
|
||||
if (fullGameStoreAppData is not null)
|
||||
{
|
||||
fullGameName = fullGameStoreAppData.Name;
|
||||
|
||||
@@ -81,9 +81,8 @@ internal sealed partial class TestGameForm : CustomForm
|
||||
|
||||
string name = await Task.Run(async () =>
|
||||
{
|
||||
// Use a dedicated client with a neutral UA so Steam's store API doesn't reject the request.
|
||||
using System.Net.Http.HttpClient client = new();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"{Program.Name}/{Program.Version}");
|
||||
// Use an isolated client with neutral UA so Steam's store API doesn't reject the request.
|
||||
using System.Net.Http.HttpClient client = HttpClientManager.CreateIsolatedClient();
|
||||
string url = $"https://store.steampowered.com/api/appdetails?appids={appId}&filters=basic";
|
||||
try
|
||||
{
|
||||
|
||||
@@ -18,6 +18,25 @@ internal static class SteamStore
|
||||
private const int CooldownGame = 600;
|
||||
private const int CooldownDlc = 1200;
|
||||
|
||||
#if DEBUG
|
||||
private static string FormatErrorLog(int attempts, string appId, string gameName, bool isDlc, string reason,
|
||||
string parentGameName = null, string parentGameAppId = null)
|
||||
{
|
||||
if (isDlc && parentGameName != null && parentGameAppId != null)
|
||||
{
|
||||
return $"[SteamQuery][Attempt {attempts}][FAILED]\n" +
|
||||
$"BaseGame: \"{parentGameName}\" ({parentGameAppId})\n" +
|
||||
$"DLC: \"{gameName}\" ({appId})\n" +
|
||||
$"Type: DLC\n" +
|
||||
$"Reason: {reason}\n" +
|
||||
"-------";
|
||||
}
|
||||
|
||||
string type = isDlc ? "DLC" : "Game";
|
||||
return $"[SteamQuery][Attempt {attempts}][FAILED] AppId: {appId} | Name: \"{gameName}\" | Type: {type} | Reason: {reason}";
|
||||
}
|
||||
#endif
|
||||
|
||||
internal static async Task<HashSet<string>> ParseDlcAppIds(StoreAppData storeAppData)
|
||||
=> await Task.Run(() =>
|
||||
{
|
||||
@@ -31,8 +50,9 @@ internal static class SteamStore
|
||||
return dlcIds;
|
||||
});
|
||||
|
||||
internal static async Task<StoreAppData> QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0)
|
||||
internal static async Task<StoreAppData> QueryStoreAPI(string appId, bool isDlc = false, int attempts = 0, string parentGameName = null, string parentGameAppId = null)
|
||||
{
|
||||
string gameName = "Unknown";
|
||||
while (!Program.Canceled)
|
||||
{
|
||||
attempts++;
|
||||
@@ -55,13 +75,14 @@ internal static class SteamStore
|
||||
if (storeAppDetails is not null)
|
||||
{
|
||||
StoreAppData data = storeAppDetails.Data;
|
||||
if (data?.Name is not null)
|
||||
gameName = data.Name;
|
||||
|
||||
if (!storeAppDetails.Success)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log(
|
||||
"Steam store query failed on attempt #" + attempts + " for " + appId +
|
||||
(isDlc ? " (DLC)" : "")
|
||||
+ ": Query unsuccessful (" + app.Value.ToString(Formatting.None) + ")",
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Query unsuccessful", parentGameName, parentGameAppId),
|
||||
LogTextBox.Warning);
|
||||
#endif
|
||||
if (data is null)
|
||||
@@ -78,9 +99,8 @@ internal static class SteamStore
|
||||
#if DEBUG
|
||||
(Exception e)
|
||||
{
|
||||
DebugForm.Current.Log("Steam store query failed on attempt #" + attempts +
|
||||
" for " + appId + (isDlc ? " (DLC)" : "")
|
||||
+ ": Unsuccessful serialization (" + e.Message + ")");
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, $"Unsuccessful serialization ({e.Message})", parentGameName, parentGameAppId));
|
||||
}
|
||||
#else
|
||||
{
|
||||
@@ -90,27 +110,24 @@ internal static class SteamStore
|
||||
return data;
|
||||
}
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Steam store query failed on attempt #" + attempts + " for " +
|
||||
appId + (isDlc ? " (DLC)" : "")
|
||||
+ ": Response data null (" +
|
||||
app.Value.ToString(Formatting.None) + ")");
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Response data null", parentGameName, parentGameAppId));
|
||||
#endif
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
DebugForm.Current.Log("Steam store query failed on attempt #" + attempts + " for " +
|
||||
appId + (isDlc ? " (DLC)" : "")
|
||||
+ ": Response details null (" +
|
||||
app.Value.ToString(Formatting.None) + ")");
|
||||
{
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Response details null", parentGameName, parentGameAppId));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
#if DEBUG
|
||||
(Exception e)
|
||||
{
|
||||
DebugForm.Current.Log("Steam store query failed on attempt #" + attempts + " for " +
|
||||
appId + (isDlc ? " (DLC)" : "")
|
||||
+ ": Unsuccessful deserialization (" + e.Message + ")");
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, $"Unsuccessful deserialization ({e.Message})", parentGameName, parentGameAppId));
|
||||
}
|
||||
#else
|
||||
{
|
||||
@@ -119,17 +136,19 @@ internal static class SteamStore
|
||||
#endif
|
||||
#if DEBUG
|
||||
else
|
||||
DebugForm.Current.Log("Steam store query failed on attempt #" + attempts + " for " + appId +
|
||||
(isDlc ? " (DLC)" : "")
|
||||
+ ": Response deserialization null");
|
||||
{
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Response deserialization null", parentGameName, parentGameAppId));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
DebugForm.Current.Log(
|
||||
"Steam store query failed on attempt #" + attempts + " for " + appId + (isDlc ? " (DLC)" : "") +
|
||||
": Response null",
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Null or empty response", parentGameName, parentGameAppId),
|
||||
LogTextBox.Warning);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -148,7 +167,8 @@ internal static class SteamStore
|
||||
if (attempts > 10)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Failed to query Steam store after 10 tries: " + appId);
|
||||
DebugForm.Current.Log(
|
||||
FormatErrorLog(attempts, appId, gameName, isDlc, "Maximum retry attempts exceeded (10)", parentGameName, parentGameAppId));
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -13,23 +13,59 @@ namespace CreamInstaller.Utility;
|
||||
|
||||
internal static class HttpClientManager
|
||||
{
|
||||
internal static HttpClient HttpClient;
|
||||
private static readonly object _lock = new();
|
||||
private static HttpClient _httpClient;
|
||||
private static SocketsHttpHandler _handler;
|
||||
|
||||
internal static HttpClient HttpClient
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _httpClient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string> HttpContentCache = new();
|
||||
|
||||
internal static void Setup()
|
||||
{
|
||||
HttpClient = new();
|
||||
if (CreamInstaller.Platforms.Epic.EpicStore.EpicBool)
|
||||
lock (_lock)
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.Add(new("EpicGamesLauncher", "18.9.0-45233261+++Portal+Release-Live"));
|
||||
CreamInstaller.Platforms.Epic.EpicStore.EpicBool = false;
|
||||
// If already set up, don't recreate to avoid socket exhaustion
|
||||
if (_httpClient != null)
|
||||
return;
|
||||
|
||||
// Create a SocketsHttpHandler with proper pooling and lifecycle settings
|
||||
_handler = new SocketsHttpHandler
|
||||
{
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(10), // Rotate connections every 10 minutes to respect DNS changes
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // Close idle connections after 2 minutes
|
||||
MaxConnectionsPerServer = 10, // Reasonable concurrent connection limit
|
||||
EnableMultipleHttp2Connections = true
|
||||
};
|
||||
|
||||
// Create HttpClient with the handler
|
||||
_httpClient = new HttpClient(_handler, disposeHandler: false)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(30) // 30 second timeout for all requests
|
||||
};
|
||||
|
||||
// Set user agent based on context
|
||||
if (CreamInstaller.Platforms.Epic.EpicStore.EpicBool)
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new("EpicGamesLauncher", "18.9.0-45233261+++Portal+Release-Live"));
|
||||
CreamInstaller.Platforms.Epic.EpicStore.EpicBool = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(new(Program.Name, Program.Version));
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new(CultureInfo.CurrentCulture.ToString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpClient.DefaultRequestHeaders.UserAgent.Add(new(Program.Name, Program.Version));
|
||||
}
|
||||
HttpClient.DefaultRequestHeaders.AcceptLanguage.Add(new(CultureInfo.CurrentCulture.ToString()));
|
||||
}
|
||||
|
||||
internal static async Task<string> EnsureGet(string url)
|
||||
@@ -52,16 +88,31 @@ internal static class HttpClientManager
|
||||
if (e.StatusCode != HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Get request failed to " + url + ": " + e, LogTextBox.Warning);
|
||||
string statusInfo = e.StatusCode.HasValue ? $" (HTTP {(int)e.StatusCode.Value})" : "";
|
||||
DebugForm.Current.Log($"Get request failed to {url}{statusInfo}: {e}", LogTextBox.Warning);
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Too many requests to " + url, LogTextBox.Error);
|
||||
DebugForm.Current.Log($"Too many requests to {url} (HTTP 429 - Rate Limited)", LogTextBox.Error);
|
||||
#endif
|
||||
// do something special?
|
||||
return null;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Get request timed out for " + url, LogTextBox.Warning);
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugForm.Current.Log("Get request was cancelled for " + url, LogTextBox.Warning);
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -88,5 +139,37 @@ internal static class HttpClientManager
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Dispose() => HttpClient?.Dispose();
|
||||
/// <summary>
|
||||
/// Creates a new HttpClient for isolated/one-off use cases.
|
||||
/// The caller is responsible for disposing the returned client.
|
||||
/// </summary>
|
||||
internal static HttpClient CreateIsolatedClient(TimeSpan? timeout = null)
|
||||
{
|
||||
var handler = new SocketsHttpHandler
|
||||
{
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(2),
|
||||
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1),
|
||||
MaxConnectionsPerServer = 5
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler, disposeHandler: true)
|
||||
{
|
||||
Timeout = timeout ?? TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"{Program.Name}/{Program.Version}");
|
||||
return client;
|
||||
}
|
||||
|
||||
internal static void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_httpClient?.Dispose();
|
||||
_httpClient = null;
|
||||
|
||||
_handler?.Dispose();
|
||||
_handler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user