// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Teams.Common.Logging; namespace Microsoft.Teams.Common.Http; public interface IHttpClient : IDisposable { public IHttpClientOptions Options { get; } public Task> SendAsync(IHttpRequest request, CancellationToken cancellationToken = default); public Task> SendAsync(IHttpRequest request, CancellationToken cancellationToken = default); } public class HttpClient : IHttpClient { public IHttpClientOptions Options { get; } protected System.Net.Http.HttpClient _client; protected ILogger _logger; private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; public HttpClient() { _client = new System.Net.Http.HttpClient(); _logger = new ConsoleLogger().Child("Http.Client"); Options = new HttpClientOptions(); Options.Apply(_client); } public HttpClient(IHttpClientOptions options) { _client = new System.Net.Http.HttpClient(); _logger = options.Logger?.Child("Http.Client") ?? new ConsoleLogger().Child("Http.Client"); Options = options; Options.Apply(_client); } public HttpClient(System.Net.Http.HttpClient client) { _client = client; _logger = new ConsoleLogger().Child("Http.Client"); Options = new HttpClientOptions(); Options.Apply(_client); } public async Task> SendAsync(IHttpRequest request, CancellationToken cancellationToken = default) { var httpRequest = CreateRequest(request); var httpResponse = await _client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); return await CreateResponse(httpResponse, cancellationToken).ConfigureAwait(false); } public async Task> SendAsync(IHttpRequest request, CancellationToken cancellationToken = default) { var httpRequest = CreateRequest(request); var httpResponse = await _client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); return await CreateResponse(httpResponse, cancellationToken).ConfigureAwait(false); } public void Dispose() { _client.Dispose(); } protected HttpRequestMessage CreateRequest(IHttpRequest request) { var httpRequest = new HttpRequestMessage( request.Method, request.Url ); Options.Apply(httpRequest); if (request.Body is not null) { if (request.Body is string stringBody) { httpRequest.Content = new StringContent(stringBody); } else if (request.Body is IEnumerable> dictionaryBody) { httpRequest.Content = new FormUrlEncodedContent(dictionaryBody); } else { string body = JsonSerializer.Serialize(request.Body, _jsonSerializerOptions); httpRequest.Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); } } foreach (var kv in request.Headers) { if (kv.Key.StartsWith("Content-") && httpRequest.Content != null) { if (kv.Key == "Content-Type") { httpRequest.Content.Headers.ContentType = new MediaTypeHeaderValue(kv.Value.First()); continue; } httpRequest.Content.Headers.TryAddWithoutValidation(kv.Key, kv.Value); continue; } httpRequest.Headers.TryAddWithoutValidation(kv.Key, kv.Value); } return httpRequest; } protected async Task> CreateResponse(HttpResponseMessage response, CancellationToken cancellationToken = default) { if (!response.IsSuccessStatusCode) { var errorBody = await ParseErrorBody(response, cancellationToken).ConfigureAwait(false); throw new HttpException() { Headers = response.Headers, StatusCode = response.StatusCode, Body = errorBody, Request = response.RequestMessage }; } var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false) ?? throw new ArgumentNullException(); return new HttpResponse() { Body = body, Headers = response.Headers, StatusCode = response.StatusCode }; } protected async Task> CreateResponse(HttpResponseMessage response, CancellationToken cancellationToken = default) { if (!response.IsSuccessStatusCode) { var errorBody = await ParseErrorBody(response, cancellationToken).ConfigureAwait(false); throw new HttpException() { Headers = response.Headers, StatusCode = response.StatusCode, Body = errorBody, Request = response.RequestMessage, }; } var body = await response.Content.ReadFromJsonAsync(cancellationToken).ConfigureAwait(false) ?? throw new ArgumentNullException(); return new HttpResponse() { Body = body, Headers = response.Headers, StatusCode = response.StatusCode }; } private async Task ParseErrorBody(HttpResponseMessage response, CancellationToken cancellationToken = default) { var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false) ?? throw new ArgumentNullException(); object errorBody = content; try { var bodyAsJson = JsonSerializer.Deserialize>(content); if (bodyAsJson is not null) { errorBody = bodyAsJson; } } catch { // content is probably not a valid json } return errorBody; } }