using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Polly; using Polly.Wrap; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; namespace Microsoft.eShopOnContainers.BuildingBlocks.Resilience.Http { /// /// HttpClient wrapper that integrates Retry and Circuit /// breaker policies when invoking HTTP services. /// Based on Polly library: https://github.com/App-vNext/Polly /// public class ResilientHttpClient : IHttpClient { private readonly HttpClient _client; private readonly ILogger _logger; private readonly Func> _policyCreator; private ConcurrentDictionary _policyWrappers; public ResilientHttpClient(Func> policyCreator, ILogger logger) { _client = new HttpClient(); _logger = logger; _policyCreator = policyCreator; _policyWrappers = new ConcurrentDictionary(); } public Task PostAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { return DoPostPutAsync(HttpMethod.Post, uri, item, authorizationToken, requestId, authorizationMethod); } public Task PutAsync(string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { return DoPostPutAsync(HttpMethod.Put, uri, item, authorizationToken, requestId, authorizationMethod); } public Task DeleteAsync(string uri, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { var origin = GetOriginFromUri(uri); return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(HttpMethod.Delete, uri); if (authorizationToken != null) { requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); } if (requestId != null) { requestMessage.Headers.Add("x-requestid", requestId); } return await _client.SendAsync(requestMessage); }); } public Task GetStringAsync(string uri, string authorizationToken = null, string authorizationMethod = "Bearer") { var origin = GetOriginFromUri(uri); return HttpInvoker(origin, async () => { var requestMessage = new HttpRequestMessage(HttpMethod.Get, uri); if (authorizationToken != null) { requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); } var response = await _client.SendAsync(requestMessage); return await response.Content.ReadAsStringAsync(); }); } private Task DoPostPutAsync(HttpMethod method, string uri, T item, string authorizationToken = null, string requestId = null, string authorizationMethod = "Bearer") { if (method != HttpMethod.Post && method != HttpMethod.Put) { throw new ArgumentException("Value must be either post or put.", nameof(method)); } // a new StringContent must be created for each retry // as it is disposed after each call var origin = GetOriginFromUri(uri); return HttpInvoker(origin, () => { var requestMessage = new HttpRequestMessage(method, uri); requestMessage.Content = new StringContent(JsonConvert.SerializeObject(item), System.Text.Encoding.UTF8, "application/json"); if (authorizationToken != null) { requestMessage.Headers.Authorization = new AuthenticationHeaderValue(authorizationMethod, authorizationToken); } if (requestId != null) { requestMessage.Headers.Add("x-requestid", requestId); } var response = _client.SendAsync(requestMessage).Result; // raise exception if HttpResponseCode 500 // needed for circuit breaker to track fails if (response.StatusCode == HttpStatusCode.InternalServerError) { throw new HttpRequestException(); } return Task.FromResult(response); }); } private async Task HttpInvoker(string origin, Func> action) { var normalizedOrigin = NormalizeOrigin(origin); if (!_policyWrappers.TryGetValue(normalizedOrigin, out PolicyWrap policyWrap)) { policyWrap = Policy.Wrap(_policyCreator(normalizedOrigin).ToArray()); _policyWrappers.TryAdd(normalizedOrigin, policyWrap); } // Executes the action applying all // the policies defined in the wrapper return await policyWrap.Execute(action, new Context(normalizedOrigin)); } private static string NormalizeOrigin(string origin) { return origin?.Trim()?.ToLower(); } private static string GetOriginFromUri(string uri) { var url = new Uri(uri); var origin = $"{url.Scheme}://{url.DnsSafeHost}:{url.Port}"; return origin; } } }