Files

116 lines
4.1 KiB
C#
Raw Permalink Normal View History

2026-08-12 14:04:55 +03:00
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),
};
}
}