116 lines
4.1 KiB
C#
116 lines
4.1 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using RudderSdk.Core.Abstractions;
|
|
using RudderSdk.Core.Models;
|
|
|
|
namespace RudderSdk.Core;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IRudderTransport"/> built on <see cref="HttpClient"/>.
|
|
/// Serializes bodies with Newtonsoft.Json, attaches the bearer token, maps
|
|
/// non-success statuses to <see cref="RudderApiException"/> subclasses and
|
|
/// network failures to <see cref="RudderNetworkException"/>.
|
|
/// </summary>
|
|
public sealed class HttpClientTransport : IRudderTransport
|
|
{
|
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10);
|
|
|
|
private readonly string _baseUrl;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
/// <summary>
|
|
/// Creates the transport for the given API base URL. When no
|
|
/// <paramref name="httpClient"/> is supplied, an owned instance with a
|
|
/// 10-second request timeout is created.
|
|
/// </summary>
|
|
public HttpClientTransport(string baseUrl, HttpClient? httpClient = null)
|
|
{
|
|
if (string.IsNullOrEmpty(baseUrl))
|
|
throw new ArgumentException("Base URL is required.", nameof(baseUrl));
|
|
|
|
_baseUrl = baseUrl.TrimEnd('/');
|
|
_httpClient = httpClient ?? new HttpClient { Timeout = DefaultTimeout };
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public async Task<TResponse> SendAsync<TRequest, TResponse>(
|
|
string method,
|
|
string path,
|
|
TRequest? request,
|
|
string? accessToken,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
using var httpRequest = new HttpRequestMessage(new HttpMethod(method), _baseUrl + path);
|
|
if (!string.IsNullOrEmpty(accessToken))
|
|
httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
|
if (request is not null)
|
|
httpRequest.Content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
|
|
|
|
HttpResponseMessage response;
|
|
try
|
|
{
|
|
response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw new RudderNetworkException("The request timed out.", ex);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
throw new RudderNetworkException(ex.Message, ex);
|
|
}
|
|
|
|
using (response)
|
|
{
|
|
var body = response.Content is null
|
|
? null
|
|
: await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
throw MapError((int)response.StatusCode, response.ReasonPhrase, body);
|
|
|
|
if (string.IsNullOrEmpty(body))
|
|
return default!;
|
|
|
|
return JsonConvert.DeserializeObject<TResponse>(body)!;
|
|
}
|
|
}
|
|
|
|
private static RudderApiException MapError(int statusCode, string? reason, string? body)
|
|
{
|
|
string? code = null;
|
|
string? message = null;
|
|
string? requestId = null;
|
|
|
|
if (!string.IsNullOrEmpty(body))
|
|
{
|
|
try
|
|
{
|
|
var error = JsonConvert.DeserializeObject<ErrorResponse>(body);
|
|
code = error?.Code;
|
|
message = error?.Error;
|
|
requestId = error?.RequestId;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Non-JSON error body — fall back to the status line.
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(message))
|
|
message = string.IsNullOrEmpty(reason) ? "HTTP " + statusCode : reason;
|
|
|
|
return statusCode switch
|
|
{
|
|
401 => new RudderAuthException(statusCode, code, message, requestId),
|
|
404 => new RudderNotFoundException(statusCode, code, message, requestId),
|
|
429 => new RudderRateLimitException(statusCode, code, message, requestId),
|
|
_ => new RudderApiException(statusCode, code, message, requestId),
|
|
};
|
|
}
|
|
}
|