using System.Net.Http.Headers; using System.Text; using System.Text.Json; namespace ResolveSpec; /// Shared HTTP configuration for both clients. public sealed class ClientOptions { public string? Token { get; set; } public Dictionary Headers { get; } = new(StringComparer.OrdinalIgnoreCase); public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30); /// Supply your own HttpClient (tests, pooling). Its BaseAddress is ignored. public HttpClient? HttpClient { get; set; } } internal sealed class Transport { public readonly string BaseUrl; readonly ClientOptions _o; readonly HttpClient _http; public Transport(string baseUrl, ClientOptions? o) { BaseUrl = baseUrl.TrimEnd('/'); _o = o ?? new ClientOptions(); _http = _o.HttpClient ?? new HttpClient { Timeout = _o.Timeout }; } /// Content-Type < custom headers < per-call headers < bearer token. public async Task<(HttpResponseMessage resp, string body)> SendAsync( HttpMethod method, string url, string? json, IDictionary? extra, CancellationToken ct) { using var req = new HttpRequestMessage(method, url); if (json != null) req.Content = new StringContent(json, Encoding.UTF8, "application/json"); foreach (var (k, v) in _o.Headers) Set(req, k, v); if (extra != null) foreach (var (k, v) in extra) Set(req, k, v); if (!string.IsNullOrEmpty(_o.Token)) req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _o.Token); var resp = await _http.SendAsync(req, ct).ConfigureAwait(false); var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); return (resp, body); } static void Set(HttpRequestMessage req, string name, string value) { req.Headers.Remove(name); if (!req.Headers.TryAddWithoutValidation(name, value) && req.Content != null) { req.Content.Headers.Remove(name); req.Content.Headers.TryAddWithoutValidation(name, value); } } public static ResolveSpecException ErrorFrom(int status, string body, string? reason) { ApiError? err = null; var isJson = false; try { using var doc = JsonDocument.Parse(body); isJson = true; if (doc.RootElement.ValueKind == JsonValueKind.Object && doc.RootElement.TryGetProperty("error", out var e) && e.ValueKind == JsonValueKind.Object) err = e.Deserialize(); } catch (JsonException) { } var message = err?.Message; if (string.IsNullOrEmpty(message)) { var text = isJson ? "" : body.Trim(); if (text.Length > 200) text = text[..200]; message = text.Length > 0 ? text : $"{reason ?? "Error"} ({status})"; } return new ResolveSpecException(message, status, err); } public static string Segment(string s) => Uri.EscapeDataString(s); }