Open hugoqribeiro opened 4 years ago
Hi @hugoqribeiro!
I had the exact same issue. Under the hood the CloudTableClient forces ClientHttpHandler as the innermost handler.
My workaround is to hack the whole thing by writing my own DelegatingHandler that actually does the authorization stuff and don't use the InnerHandler, but instead an injected SocketsHttpHandler. This works fine, but it's frustrating to have to hack the SDK this way.
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Azure.Cosmos.Table;
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MsgStatsTrigger
{
public class CosmosTableHttpHandler : DelegatingHandler
{
private readonly MethodInfo _sendAsync;
private readonly HttpMessageHandler _messageHandler;
private readonly StorageCredentials _credentials;
public CosmosTableHttpHandler(StorageCredentials credentials, HttpMessageHandler messageHandler) : base()
{
_credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
_messageHandler = messageHandler ?? throw new ArgumentNullException(nameof(messageHandler));
_sendAsync = messageHandler.GetType().GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_ = request ?? throw new ArgumentNullException(nameof(request));
if (!request.Headers.Contains("x-ms-date"))
request.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture));
if (_credentials.IsSharedKey)
{
var signature = ComputeHmacSignature(GetCanonicalizedHttpRequestMessage(request));
request.Headers.Authorization = new AuthenticationHeaderValue("SharedKey", $"{_credentials.AccountName}:{signature}");
}
return await (_sendAsync.Invoke(_messageHandler, new object[] { request, cancellationToken }) as Task<HttpResponseMessage>);
}
private string ComputeHmacSignature(string message)
{
using (var hmac = new HMACSHA256(Convert.FromBase64String(_credentials.Key)))
{
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(message)));
}
}
private string GetCanonicalizedHttpRequestMessage(HttpRequestMessage request)
{
_ = request ?? throw new ArgumentNullException(nameof(request));
var canonicalizedString = new StringBuilder(request.Method.Method);
canonicalizedString.Append('\n');
if (request.Content != null && request.Content.Headers.ContentMD5 != null)
canonicalizedString.Append(Convert.ToBase64String(request.Content.Headers.ContentMD5));
canonicalizedString.Append('\n');
if (request.Content != null && request.Content.Headers.ContentType != null)
canonicalizedString.Append(request.Content.Headers.ContentType.ToString());
var msDateHeader = request.Headers.Contains("x-ms-date") ? request.Headers.GetValues("x-ms-date").First() : string.Empty;
canonicalizedString.Append('\n');
if (!string.IsNullOrEmpty(msDateHeader))
canonicalizedString.Append(msDateHeader);
else if (request.Headers.Date.HasValue)
canonicalizedString.Append(request.Headers.Date.Value.UtcDateTime.ToString("R", CultureInfo.InvariantCulture));
canonicalizedString.Append("\n/");
var comp = GetQueryString(request.RequestUri.Query, "comp") ?? "";
canonicalizedString.Append(_credentials.AccountName);
canonicalizedString.Append(request.RequestUri.AbsolutePath.Replace("-secondary", "", StringComparison.Ordinal));
if (comp != string.Empty)
canonicalizedString.Append($"?comp={comp}");
return canonicalizedString.ToString();
}
private static string GetQueryString(string queryString, string name)
{
var query = QueryHelpers.ParseQuery(queryString);
if (!query.TryGetValue(name, out var values))
return null;
return values[0];
}
}
}
Here's how I configure the CloudTableClient:
var storageAccount = CloudStorageAccount.Parse("connection-string-goes-here");
var tableClient = storageAccount.CreateCloudTableClient();
var socketsHandler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(10),
MaxConnectionsPerServer = 50,
ConnectTimeout = TimeSpan.FromSeconds(30),
};
var handler = new CosmosTableHttpHandler(storageAccount.Credentials, socketsHandler);
tableClient.TableClientConfiguration.RestExecutorConfiguration.DelegatingHandler = handler;
It seems like this issue hasn't been noticed AT ALL by Microsoft, so I'll create another one and see if that gets any traction.
To have the SDK cater for IHttpClientFactory or your own implementation for SocketsHttpHandler is really needed. CosmosDB's sdk has a really nice implementation. Would be great if table storage also did.
Thanks for the code though @HansOlavS ! But yea, really would be so much better if the SDK came out with its own.
.... if it does and I'm just not googling well, please point me in the right direction! Much thanks!
Check out the Azure.Data.Tables nuget! I checked out a beta version way back and it looked really nice! (forget my old post!!)
Thanks for that! Had to rewrite/discard a lot of our code to work with the new package but with that I was able to use IHttpClientFactory to provide an instance of HttpClient for everything that needed it in our Api i.e. Cosmos, Table, and Blob. It prevented Socket Exhaustion issues and with a bit of Dependency Injection rework we were able to get a very efficient API.
Hi. I'm using the Microsoft.Azure.Cosmos.Table package (version 1.0.6).
I intended to change the way
CloudTableClient
is initialized to take advantage ofIHttpMessageHandlerFactory
because we're facing scenarios of sockets exaustion derived from accesses to the table storage (the service that usesCloudTableClient
is transient and there is no viable path to make it singleton).The problem is that I don't see any way to do it.
RestExecutorConfiguration
accepts aDelegatingHandler
but it requires it to have a null inner handler. I don't see any way to pass in theHttpMessageHandler
provided byIHttpMessageHandlerFactory
.Is this something that will change in the future? Any plans to support reusing
HttpClient
orHttpMessageHandler
?Thanks.