// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using EpicGames.Horde.Jobs; #pragma warning disable CA2234 // Use Uri instead of string namespace EpicGames.Horde { /// /// Static helper methods for implementing Horde HTTP requests with standard semantics /// public static class HordeHttpRequest { static readonly JsonSerializerOptions s_jsonSerializerOptions = CreateJsonSerializerOptions(); internal static JsonSerializerOptions JsonSerializerOptions => s_jsonSerializerOptions; /// /// Create the shared instance of JSON options for HordeHttpClient instances /// static JsonSerializerOptions CreateJsonSerializerOptions() { JsonSerializerOptions options = new JsonSerializerOptions(); ConfigureJsonSerializer(options); return options; } /// /// Configures a JSON serializer to read Horde responses /// /// options for the serializer public static void ConfigureJsonSerializer(JsonSerializerOptions options) { options.AllowTrailingCommas = true; options.ReadCommentHandling = JsonCommentHandling.Skip; options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.PropertyNameCaseInsensitive = true; options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new StringIdJsonConverterFactory()); options.Converters.Add(new BinaryIdJsonConverterFactory()); options.Converters.Add(new SubResourceIdJsonConverterFactory()); } /// /// Deletes a resource from an HTTP endpoint /// /// Http client instance /// The url to retrieve /// Cancels the request public static async Task DeleteAsync(HttpClient httpClient, string relativePath, CancellationToken cancellationToken = default) { using HttpResponseMessage response = await httpClient.DeleteAsync(relativePath, cancellationToken); response.EnsureSuccessStatusCode(); } /// /// Gets a resource from an HTTP endpoint and parses it as a JSON object /// /// The object type to return /// Http client instance /// The url to retrieve /// Cancels the request /// New instance of the object public static async Task GetAsync(HttpClient httpClient, string relativePath, CancellationToken cancellationToken = default) { TResponse? response = await httpClient.GetFromJsonAsync(relativePath, s_jsonSerializerOptions, cancellationToken); return response ?? throw new InvalidCastException($"Expected non-null response from GET to {relativePath}"); } /// /// Posts an object to an HTTP endpoint as a JSON object, and parses the response object /// /// The object type to post /// Http client instance /// The url to retrieve /// The object to post /// Cancels the request /// The response parsed into the requested type internal static async Task PostAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken = default) { return await httpClient.PostAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken); } /// /// Posts an object to an HTTP endpoint as a JSON object, and parses the response object /// /// The object type to return /// The object type to post /// Http client instance /// The url to retrieve /// The object to post /// Cancels the request /// The response parsed into the requested type public static async Task PostAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken = default) { using (HttpResponseMessage response = await PostAsync(httpClient, relativePath, request, cancellationToken)) { if (!response.IsSuccessStatusCode) { string body = await response.Content.ReadAsStringAsync(cancellationToken); throw new HttpRequestException($"{(int)response.StatusCode} ({response.StatusCode}) posting to {new Uri(httpClient.BaseAddress!, relativePath)}: {body}", null, response.StatusCode); } TResponse? responseValue = await response.Content.ReadFromJsonAsync(s_jsonSerializerOptions, cancellationToken); return responseValue ?? throw new InvalidCastException($"Expected non-null response from POST to {relativePath}"); } } /// /// Puts an object to an HTTP endpoint as a JSON object /// /// The object type to post /// Http client instance /// The url to write to /// The object to post /// Cancels the request /// Response message public static async Task PutAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken) { return await httpClient.PutAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken); } /// /// Puts an object to an HTTP endpoint as a JSON object /// /// The object type to return /// The object type to post /// Http client instance /// The url to write to /// The object to post /// Cancels the request /// Response message public static async Task PutAsync(HttpClient httpClient, string relativePath, TRequest request, CancellationToken cancellationToken) { using (HttpResponseMessage response = await httpClient.PutAsJsonAsync(relativePath, request, s_jsonSerializerOptions, cancellationToken)) { if (!response.IsSuccessStatusCode) { string body = await response.Content.ReadAsStringAsync(cancellationToken); throw new HttpRequestException($"{response.StatusCode} put to {new Uri(httpClient.BaseAddress!, relativePath)}: {body}", null, response.StatusCode); } TResponse? responseValue = await response.Content.ReadFromJsonAsync(s_jsonSerializerOptions, cancellationToken); return responseValue ?? throw new InvalidCastException($"Expected non-null response from PUT to {relativePath}"); } } } }