using System.Net.Http.Headers; using Microsoft.Extensions.Logging; namespace Infrastructure.StockData; /// /// KOSIS(kosis.kr) OpenAPI 호출 공통 — 인증은 URL query param apiKey (헤더 아님, koreaexim/OpenDART 와 동일 방식). /// 호출부가 apiKey 를 포함한 완성 URL 을 넘기며, 여기선 Accept json + 간단 3회 재시도(2s/4s 백오프) + 로그 시 키 마스킹만 담당. /// internal static class KosisHttp { public const string ClientName = "Kosis"; private const int MaxAttempts = 3; public static async Task GetStringWithRetryAsync(HttpClient client, string url, ILogger logger, CancellationToken ct) { for (var attempt = 1; ; attempt++) { try { using var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using var response = await client.SendAsync(request, ct); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(ct); } catch (Exception ex) when (ex is not OperationCanceledException && attempt < MaxAttempts) { logger.LogWarning(ex, "[Kosis] HTTP 실패 — 재시도 {Attempt}/{Max} (url={Url})", attempt, MaxAttempts, Mask(url)); await Task.Delay(TimeSpan.FromSeconds(2 * attempt), ct); } } } /// 로그용 URL 에서 apiKey 값을 가린다. public static string Mask(string url) { var idx = url.IndexOf("apiKey=", StringComparison.OrdinalIgnoreCase); if (idx < 0) { return url; } var start = idx + "apiKey=".Length; var end = url.IndexOf('&', start); if (end < 0) { end = url.Length; } return string.Concat(url.AsSpan(0, start), "***", url.AsSpan(end)); } }