Browse Source

added a HttpClientFactoryClient and HttpClientFactoryClientProvider that use IHttpClientFactory to create HttpClients to avoid having to recreate an HtppClient every time a DiscordRestClient is created which could lead to socket exhaustation.

pull/1806/head
emorell96 4 years ago
parent
commit
87f530c2f1
2 changed files with 206 additions and 0 deletions
  1. +179
    -0
      src/Discord.Net.Rest/Net/HttpClientFactoryClient.cs
  2. +27
    -0
      src/Discord.Net.Rest/Net/HttpClientFactoryClientProvider.cs

+ 179
- 0
src/Discord.Net.Rest/Net/HttpClientFactoryClient.cs View File

@@ -0,0 +1,179 @@
using Discord.Net.Converters;
using Discord.Net.Rest;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Rest.Net
{
internal sealed class HttpClientFactoryClient : IRestClient, IDisposable
{
private const int HR_SECURECHANNELFAILED = -2146233079;

private readonly HttpClient _client;
private readonly string _baseUrl;
private readonly JsonSerializer _errorDeserializer;
private CancellationToken _cancelToken;
private bool _isDisposed;

public HttpClientFactoryClient(string baseUrl, HttpClient httpClient, bool useProxy = false)
{
_baseUrl = baseUrl;
//this client would be given to use through DI. The advantage would be that it would be managed by DI, and no socket Exhaustation possible.
_client = httpClient;
//#pragma warning disable IDISP014
// _client = new HttpClient(new HttpClientHandler
// {
// AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
// UseCookies = false,
// UseProxy = useProxy,
// });
//#pragma warning restore IDISP014
SetHeader("accept-encoding", "gzip, deflate");

_cancelToken = CancellationToken.None;
_errorDeserializer = new JsonSerializer();
}
private void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
_client.Dispose();
_isDisposed = true;
}
}
public void Dispose()
{
Dispose(true);
}

public void SetHeader(string key, string value)
{
_client.DefaultRequestHeaders.Remove(key);
if (value != null)
_client.DefaultRequestHeaders.Add(key, value);
}
public void SetCancelToken(CancellationToken cancelToken)
{
_cancelToken = cancelToken;
}

public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null)
restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null)
restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
}

/// <exception cref="InvalidOperationException">Unsupported param type.</exception>
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null)
{
string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null)
restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
MemoryStream memoryStream = null;
if (multipartParams != null)
{
foreach (var p in multipartParams)
{
switch (p.Value)
{
#pragma warning disable IDISP004
case string stringValue:
{ content.Add(new StringContent(stringValue), p.Key); continue; }
case byte[] byteArrayValue:
{ content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; }
case Stream streamValue:
{ content.Add(new StreamContent(streamValue), p.Key); continue; }
case MultipartFile fileValue:
{
var stream = fileValue.Stream;
if (!stream.CanSeek)
{
memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
#pragma warning disable IDISP001
stream = memoryStream;
#pragma warning restore IDISP001
}
content.Add(new StreamContent(stream), p.Key, fileValue.Filename);
#pragma warning restore IDISP004
continue;
}
default:
throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\".");
}
}
}
restRequest.Content = content;
var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
memoryStream?.Dispose();
return result;
}
}

private async Task<RestResponse> SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly)
{
using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken))
{
cancelToken = cancelTokenSource.Token;
HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false);

var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase);

var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null;

return new RestResponse(response.StatusCode, headers, stream);
}
}

private static readonly HttpMethod Patch = new HttpMethod("PATCH");
private HttpMethod GetMethod(string method)
{
switch (method)
{
case "DELETE":
return HttpMethod.Delete;
case "GET":
return HttpMethod.Get;
case "PATCH":
return Patch;
case "POST":
return HttpMethod.Post;
case "PUT":
return HttpMethod.Put;
default:
throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}");
}
}
}
}

+ 27
- 0
src/Discord.Net.Rest/Net/HttpClientFactoryClientProvider.cs View File

@@ -0,0 +1,27 @@
using Discord.Net.Rest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Discord.Rest.Net
{
public class HttpFactoryRestClientProvider
{
public readonly RestClientProvider Instance;

private readonly IHttpClientFactory _httpClientFactory;

public HttpFactoryRestClientProvider(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;

Instance = url => new HttpClientFactoryClient(url, _httpClientFactory.CreateClient("HttpFactoryRestClientProvider"), useProxy: false);
}



}
}

Loading…
Cancel
Save