feat(web): implement exponential backoff retry pattern in KisApiClient
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 23s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled
Prepare Release / Build & Create Release (push) Successful in 1m3s
Prepare Release / Release Notification (push) Successful in 1s
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 23s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled
Prepare Release / Build & Create Release (push) Successful in 1m3s
Prepare Release / Release Notification (push) Successful in 1s
This commit is contained in:
@@ -141,23 +141,37 @@ public class KisApiClient : IKisApiClient
|
|||||||
if (!string.IsNullOrEmpty(queryString))
|
if (!string.IsNullOrEmpty(queryString))
|
||||||
url += $"?{queryString}";
|
url += $"?{queryString}";
|
||||||
|
|
||||||
try
|
int maxAttempts = 3;
|
||||||
{
|
int delayMs = 1000;
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
|
||||||
foreach (var header in headers)
|
|
||||||
request.Headers.Add(header.Key, header.Value);
|
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
|
||||||
return result ?? new Dictionary<string, object>();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "KIS request failed: {Path} / {TrId}", path, trId);
|
try
|
||||||
throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId}.", ex);
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.Headers.Add(header.Key, header.Value);
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(request);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||||
|
return result ?? new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < maxAttempts)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "KIS request failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
|
||||||
|
await Task.Delay(delayMs);
|
||||||
|
delayMs *= 2;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "KIS request failed after {MaxAttempts} attempts: {Path} / {TrId}", maxAttempts, path, trId);
|
||||||
|
throw new InvalidOperationException($"KIS read-only request failed for {path} / {trId} after {maxAttempts} attempts.", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Unreachable code in KIS client SendRequestAsync");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
|
private async Task<string> GetOrRefreshTokenAsync(KisCredentials creds)
|
||||||
@@ -174,38 +188,51 @@ public class KisApiClient : IKisApiClient
|
|||||||
|
|
||||||
var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };
|
var tokenRequest = new { grant_type = "client_credentials", appkey = creds.AppKey, appsecret = creds.AppSecret };
|
||||||
|
|
||||||
try
|
int maxAttempts = 3;
|
||||||
|
int delayMs = 1000;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||||
{
|
{
|
||||||
var response = await _httpClient.PostAsJsonAsync(
|
try
|
||||||
$"{creds.Domain}/oauth2/tokenP",
|
{
|
||||||
tokenRequest
|
var response = await _httpClient.PostAsJsonAsync(
|
||||||
);
|
$"{creds.Domain}/oauth2/tokenP",
|
||||||
response.EnsureSuccessStatusCode();
|
tokenRequest
|
||||||
|
);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
var tokenData = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||||
if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
|
if (tokenData == null) throw new InvalidOperationException("Token response body is empty");
|
||||||
|
|
||||||
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
|
if (!tokenData.TryGetValue("access_token", out var tokenObj) || tokenObj == null)
|
||||||
throw new InvalidOperationException("No access_token in response");
|
throw new InvalidOperationException("No access_token in response");
|
||||||
var accessToken = tokenObj.ToString()!;
|
var accessToken = tokenObj.ToString()!;
|
||||||
|
|
||||||
var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
|
var expiresInStr = tokenData.TryGetValue("expires_in", out var expiresObj) && expiresObj != null
|
||||||
? expiresObj.ToString()
|
? expiresObj.ToString()
|
||||||
: "86400";
|
: "86400";
|
||||||
var expiresInSec = int.TryParse(expiresInStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)
|
var expiresInSec = int.TryParse(expiresInStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seconds)
|
||||||
? seconds
|
? seconds
|
||||||
: 86400;
|
: 86400;
|
||||||
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
|
var expiresAt = DateTime.UtcNow.AddSeconds(expiresInSec);
|
||||||
|
|
||||||
await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt);
|
await _tokenCache.SaveTokenAsync(creds.Account, accessToken, expiresAt);
|
||||||
_logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt);
|
_logger.LogInformation("KIS token refreshed for {Account}; expires at {ExpiresAtUtc}", creds.Account, expiresAt);
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) when (attempt < maxAttempts)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "KIS token refresh failed");
|
_logger.LogWarning(ex, "KIS token refresh failed on attempt {Attempt}/{MaxAttempts}. Retrying in {Delay}ms...", attempt, maxAttempts, delayMs);
|
||||||
throw new InvalidOperationException("KIS token refresh failed; check credentials and API availability.", ex);
|
await Task.Delay(delayMs);
|
||||||
|
delayMs *= 2;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "KIS token refresh failed after {MaxAttempts} attempts", maxAttempts);
|
||||||
|
throw new InvalidOperationException($"KIS token refresh failed after {maxAttempts} attempts; check credentials and API availability.", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
throw new InvalidOperationException("Unreachable code in KIS client TokenRefresh");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user