RestClient
using Polly;
using Relativity.Transfer.Resources;
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Relativity.Transfer
{
public class RestClient
{
private const HttpStatusCode NoHttpStatusCode = (HttpStatusCode)0;
private const int HttpRequestGet = 1;
private const int HttpRequestPost = 2;
private const int HttpRequestDelete = 3;
private readonly HttpConnectionInfo connectionInfo;
private readonly ITransferLog transferLog;
public int MaxRetryAttempts { get; set; }
public double TimeoutSeconds { get; }
public RestClient(HttpConnectionInfo connectionInfo, ITransferLog log)
: this(connectionInfo, log, 300, 5)
{
}
public RestClient(HttpConnectionInfo connectionInfo, ITransferLog log, double timeoutSeconds, int maxRetryAttempts)
{
if (connectionInfo == null)
throw new ArgumentNullException("connectionInfo");
if (log == null)
throw new ArgumentNullException("log");
this.connectionInfo = connectionInfo;
transferLog = log;
TimeoutSeconds = timeoutSeconds;
MaxRetryAttempts = maxRetryAttempts;
}
public async Task<T> RequestDeleteAsync<T>(string endpoint, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, CancellationToken token) where T : class, new
{
return SerializationHelper.DeserializeFromJson<T>(await RequestAsync(3, endpoint, string.Empty, sleepDurationProvider, onRetry, onEndpointErrorTitle, onEndpointErrorMessage, token).ConfigureAwait(false));
}
public async Task<T> RequestGetAsync<T>(string endpoint, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, CancellationToken token) where T : class, new
{
return SerializationHelper.DeserializeFromJson<T>(await RequestAsync(1, endpoint, string.Empty, sleepDurationProvider, onRetry, onEndpointErrorTitle, onEndpointErrorMessage, token).ConfigureAwait(false));
}
public async Task<T> RequestPostAsync<T>(string endpoint, string content, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, CancellationToken token) where T : class, new
{
return SerializationHelper.DeserializeFromJson<T>(await RequestAsync(2, endpoint, content, sleepDurationProvider, onRetry, onEndpointErrorTitle, onEndpointErrorMessage, token).ConfigureAwait(false));
}
public Task<string> RequestJsonPostAsync(string endpoint, string content, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, CancellationToken token)
{
return RequestAsync(2, endpoint, content, sleepDurationProvider, onRetry, onEndpointErrorTitle, onEndpointErrorMessage, token);
}
private async Task<string> RequestAsync(int method, string endpoint, string content, Func<int, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, CancellationToken token)
{
WebException webException;
HttpWebResponse response;
return await RetrySyntaxAsync.WaitAndRetryAsync(Policy.Handle<TransferException>((Func<TransferException, bool>)((TransferException e) => !e.Fatal)), MaxRetryAttempts, sleepDurationProvider, onRetry).ExecuteAsync<string>((Func<CancellationToken, Task<string>>)async delegate {
ServicePointManager.SecurityProtocol = (SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12);
using (HttpClient client = new HttpClient()) {
client.BaseAddress = connectionInfo.Host;
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("X-CSRF-Header", string.Empty);
string authenticationHeader = connectionInfo.Credential.GetAuthenticationHeader();
if (string.IsNullOrEmpty(authenticationHeader))
throw CreateCredentialNotSupportedTransferException(endpoint, onEndpointErrorTitle, connectionInfo.Credential);
client.DefaultRequestHeaders.Add("Authorization", authenticationHeader);
if (TimeoutSeconds > 0)
client.Timeout = TimeSpan.FromSeconds(TimeoutSeconds);
using (StringContent stringContent = new StringContent((!string.IsNullOrEmpty(content)) ? content : string.Empty, Encoding.UTF8, "application/json")) {
Uri endpointUri = new Uri(endpoint, UriKind.Relative);
string methodName = null;
try {
HttpResponseMessage response2;
switch (method) {
case 1:
methodName = "GET";
transferLog.LogDebug("Preparing to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint...", endpointUri, methodName);
response2 = await client.GetAsync(endpointUri, token).ConfigureAwait(false);
break;
case 2:
methodName = "POST";
transferLog.LogDebug("Preparing to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint...", endpointUri, methodName);
response2 = await client.PostAsync(endpointUri, stringContent, token).ConfigureAwait(false);
break;
case 3:
methodName = "DELETE";
transferLog.LogDebug("Preparing to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint...", endpointUri, methodName);
response2 = await client.DeleteAsync(endpointUri, token).ConfigureAwait(false);
break;
default:
throw new NotSupportedException($"""{method}""");
}
return await ReadResponseAsync(endpointUri, methodName, response2, onEndpointErrorTitle, onEndpointErrorMessage).ConfigureAwait(false);
} catch (TaskCanceledException exception) {
if (token.IsCancellationRequested)
throw;
throw CreateTimeoutTransferException(endpoint, methodName, exception, onEndpointErrorTitle, onEndpointErrorMessage);
} catch (HttpRequestException ex) {
webException = (ex.InnerException as WebException);
if (webException != null) {
if (GlobalSettings.Instance.FatalWebExceptionStatusCodes.Any((WebExceptionStatus x) => x == webException.Status))
throw CreateExtendedTransferException(endpoint, methodName, string.Empty, webException, onEndpointErrorTitle, onEndpointErrorMessage, true);
response = (webException.Response as HttpWebResponse);
if (response != null) {
bool fatal = GlobalSettings.Instance.FatalHttpStatusCodes.Any((HttpStatusCode x) => x == response.StatusCode);
throw CreateExtendedTransferException(endpoint, methodName, response.StatusCode, string.Empty, webException, onEndpointErrorTitle, onEndpointErrorMessage, fatal);
}
throw CreateExtendedTransferException(endpoint, methodName, string.Empty, webException, onEndpointErrorTitle, onEndpointErrorMessage, false);
}
throw CreateExtendedTransferException(endpoint, methodName, (HttpStatusCode)0, string.Empty, ex, onEndpointErrorTitle, onEndpointErrorMessage, false);
}
}
}
}, token).ConfigureAwait(false);
}
private async Task<string> ReadResponseAsync(Uri endpoint, string methodName, HttpResponseMessage response, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage)
{
string json = string.Empty;
try {
json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
response.EnsureSuccessStatusCode();
transferLog.LogDebug("Successfully called the HTTP '{Endpoint}' ({HttpMethod}) endpoint.", endpoint, methodName);
return json;
} catch (OperationCanceledException exception) {
transferLog.LogInformation(exception, "The user cancelled the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation.", endpoint, methodName);
throw;
} catch (Exception exception2) {
bool fatal = GlobalSettings.Instance.FatalHttpStatusCodes.Any((HttpStatusCode candidate) => candidate == response.StatusCode);
throw CreateExtendedTransferException(endpoint.ToString(), methodName, response.StatusCode, json, exception2, onEndpointErrorTitle, onEndpointErrorMessage, fatal);
}
}
private TransferException CreateExtendedTransferException(string endpoint, string methodName, HttpStatusCode statusCode, string json, Exception exception, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, bool fatal)
{
transferLog.LogError(exception, fatal ? "Fatal attempt to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation. HTTP StatusCode={StatusCode}, Response={JsonResponse}" : "Failed to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation. HTTP StatusCode={StatusCode}, Response={JsonResponse}", endpoint, methodName, statusCode, json);
string text = onEndpointErrorTitle(statusCode);
if (string.IsNullOrEmpty(text))
text = CoreStrings.NoEndpointProvided;
string text2 = onEndpointErrorMessage(statusCode);
if (string.IsNullOrEmpty(text2))
text2 = CoreStrings.NoMessageProvided;
string text3 = GlobalSettings.Instance.FatalHttpStatusCodeDetailedMessage(statusCode);
if (string.IsNullOrEmpty(text3))
text3 = exception.Message;
if (string.IsNullOrEmpty(text3))
text3 = CoreStrings.NoMessageProvided;
string text4 = string.Format(CultureInfo.CurrentCulture, CoreStrings.HttpExceptionMessage, text, methodName, (int)statusCode, text2, text3);
if (statusCode == (HttpStatusCode)0)
text4 = string.Format(CultureInfo.CurrentCulture, CoreStrings.HttpNoStatusExceptionMessage, text, methodName, text2, text3);
text4 = text4.TrimEnd(Array.Empty<char>());
return new TransferException(text4, exception, fatal);
}
private TransferException CreateExtendedTransferException(string endpoint, string methodName, string json, WebException exception, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage, bool fatal)
{
transferLog.LogError(exception, fatal ? "Fatal attempt to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation. Web Response Status={WebResponseStatus}, Response={JsonResponse}" : "Failed to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation. Web Response Status={WebResponseStatus}, Response={JsonResponse}", endpoint, methodName, exception.Status, json);
string text = onEndpointErrorTitle((HttpStatusCode)0);
if (string.IsNullOrEmpty(text))
text = CoreStrings.NoEndpointProvided;
string text2 = onEndpointErrorMessage((HttpStatusCode)0);
if (string.IsNullOrEmpty(text2))
text2 = CoreStrings.NoMessageProvided;
string text3 = GlobalSettings.Instance.FatalWebExceptionStatusCodeDetailedMessage(exception.Status);
if (string.IsNullOrEmpty(text3))
text3 = exception.Message;
if (string.IsNullOrEmpty(text3))
text3 = CoreStrings.NoMessageProvided;
return new TransferException(string.Format(CultureInfo.CurrentCulture, CoreStrings.WebExceptionMessage, text, methodName, (int)exception.Status, text2, text3).TrimEnd(Array.Empty<char>()), exception, fatal);
}
private TransferException CreateTimeoutTransferException(string endpoint, string methodName, Exception exception, Func<HttpStatusCode, string> onEndpointErrorTitle, Func<HttpStatusCode, string> onEndpointErrorMessage)
{
transferLog.LogError(exception, "Failed to call the HTTP '{Endpoint}' ({HttpMethod}) endpoint operation because it exceeded the {HttpTimeoutSeconds} second timeout.", endpoint, methodName, TimeoutSeconds);
string text = onEndpointErrorTitle(HttpStatusCode.RequestTimeout);
if (string.IsNullOrEmpty(text))
text = CoreStrings.NoEndpointProvided;
string text2 = onEndpointErrorMessage(HttpStatusCode.RequestTimeout);
if (string.IsNullOrEmpty(text2))
text2 = CoreStrings.NoMessageProvided;
string text3 = exception.Message;
if (string.IsNullOrEmpty(text3))
text3 = CoreStrings.NoMessageProvided;
return new TransferException(string.Format(CultureInfo.CurrentCulture, CoreStrings.HttpTimeoutExceptionMessage, text, methodName, TimeoutSeconds, text2, text3).TrimEnd(Array.Empty<char>()), exception, false);
}
private TransferException CreateCredentialNotSupportedTransferException(string endpoint, Func<HttpStatusCode, string> onEndpointErrorTitle, IHttpCredential credential)
{
transferLog.LogError("Failed to call the HTTP '{Endpoint}' endpoint operation because the supplied Transfer API credential object '{CredentialType}' is not supported.", endpoint, credential.GetType());
string text = onEndpointErrorTitle(HttpStatusCode.Unauthorized);
if (string.IsNullOrEmpty(text))
text = CoreStrings.NoEndpointProvided;
return new TransferException(string.Format(CultureInfo.CurrentCulture, CoreStrings.HttpCredentialNotSupportedExceptionMessage, text, credential.GetType().ToString()).TrimEnd(Array.Empty<char>()), true);
}
}
}