using System; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using PhotoboothUploader.Models; namespace PhotoboothUploader.Services; public sealed class PhotoboothConnectClient { private const int MaxRetries = 2; private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true, }; public PhotoboothConnectClient(string baseUrl, string userAgent) { _httpClient = new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = DefaultTimeout, }; _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent); } public async Task RedeemAsync(string code, CancellationToken cancellationToken = default) { var request = new { code }; for (var attempt = 0; attempt <= MaxRetries; attempt++) { try { using var response = await _httpClient.PostAsJsonAsync("/api/v1/photobooth/connect", request, cancellationToken); var payload = await ReadPayloadAsync(response, cancellationToken); if (response.IsSuccessStatusCode) { return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen."); } if (response.StatusCode is HttpStatusCode.UnprocessableEntity or HttpStatusCode.Conflict or HttpStatusCode.Unauthorized) { return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen."); } if (attempt < MaxRetries && IsTransientStatus(response.StatusCode)) { await Task.Delay(GetRetryDelay(attempt), cancellationToken); continue; } return payload ?? Fail(response.ReasonPhrase ?? "Verbindung fehlgeschlagen."); } catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) { if (attempt < MaxRetries) { await Task.Delay(GetRetryDelay(attempt), cancellationToken); continue; } return Fail("Zeitüberschreitung bei der Verbindung."); } catch (HttpRequestException) { if (attempt < MaxRetries) { await Task.Delay(GetRetryDelay(attempt), cancellationToken); continue; } return Fail("Netzwerkfehler. Bitte Verbindung prüfen."); } catch (JsonException) { return Fail("Serverantwort konnte nicht gelesen werden."); } } return Fail("Verbindung fehlgeschlagen."); } private async Task ReadPayloadAsync(HttpResponseMessage response, CancellationToken cancellationToken) { if (response.Content.Headers.ContentLength == 0) { return null; } return await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken); } private static bool IsTransientStatus(HttpStatusCode statusCode) { return statusCode is HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests or HttpStatusCode.BadGateway or HttpStatusCode.ServiceUnavailable or HttpStatusCode.GatewayTimeout or HttpStatusCode.InternalServerError; } private static TimeSpan GetRetryDelay(int attempt) { return TimeSpan.FromMilliseconds(500 * (attempt + 1)); } private static PhotoboothConnectResponse Fail(string message) { return new PhotoboothConnectResponse { Message = message, }; } }