HttpClient 避免在异步请求期间处理

HttpClient avoid disposing during async request

我正在使用 HttpClient.SendAsync() 在 .NET Framework (C#) 中发送 restful 服务请求。

在某些情况下,我想记录请求和响应,例如当响应代码是特定的 HTTP 状态代码时,我想记录请求和响应。

请求(当然)是 HttpRequestMessage 类型,响应是 HttpResponseMessage.

类型

这是我现在正在使用的代码:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    HttpResponseMessage response = null;
    Exception exception = null;
    try
    {
        response = await InstanceClient.SendAsync(request ?? throw new ArgumentNullException(nameof(request)), completionOption, cancellationToken);
    }
    catch (Exception e)
    {
        exception = e;
        throw;
    }
    finally
    {
        await LogDelegateInvoker(logContext, request, response, exception, cancellationToken);
    }
        
    return response;
}

问题是有时在我的 LogDelegateInvoker 被调用时,HttpClient.SendAsync() 代码已经处理了请求或请求内容,这样如果我的请求试图检查(和log) 那个内容,它不能因为内容已经被处理了。

我希望有一种方法可以告诉 SendAsync() 方法不要处理该请求,我将负责自行处理它。

有什么方法可以做到这一点,或者有其他选择吗?

我找到了一个我可以接受的答案,可能对其他人有用。

我已将问题中的代码替换为:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    LoggingForHttpHandler.AssociateInvokerWithRequest(request ?? throw new ArgumentNullException(nameof(request)), 
        async (req, res, ex, ct) => await LogDelegateInvoker(logContext, req, res, ex, ct));
    return await HttpClient.SendAsync(request, completionOption, cancellationToken);
}

然后在同一个 class 中,我有额外的私有 child class 和一些静态处理:

...

private static HttpClient HttpClient => _httpClient ?? (_httpClient = new HttpClient(new LoggingForHttpHandler(new HttpClientHandler())));
private static HttpClient _httpClient;

private class LoggingForHttpHandler : DelegatingHandler
{
    public LoggingForHttpHandler(HttpMessageHandler innerHandler) : base(innerHandler)
    {
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = null;
        Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task> logDelegateInvoker = null;

        try
        {
            if (request.Headers.TryGetValues(SpecialHeaderName, out IEnumerable<string> specialHeaderValues))
            {
                request.Headers.Remove(SpecialHeaderName);
                if (ulong.TryParse(specialHeaderValues.FirstOrDefault(), out ulong logDelegateInvokerKey))
                {
                    if (!LogDelegateInvokers.TryRemove(logDelegateInvokerKey, out logDelegateInvoker))
                    {
                        logDelegateInvoker = null;
                    }
                }
            }

            response = await base.SendAsync(request, cancellationToken);
        }
        catch(Exception ex)
        {
            if (logDelegateInvoker != null)
            {
                await logDelegateInvoker(request, response, ex, cancellationToken);
            }
            throw;
        }

        if (logDelegateInvoker != null)
        {
            await logDelegateInvoker(request, response, null, cancellationToken);
        }

        return response;
    }

    public static void AssociateInvokerWithRequest (HttpRequestMessage request, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task> logDelegateInvoker)
    {
        if (logDelegateInvoker != null && request != null)
        {
            ulong logDelegateInvokerKey = (ulong)(Interlocked.Increment(ref _incrementer) - long.MinValue);
            if (LogDelegateInvokers.TryAdd(logDelegateInvokerKey, logDelegateInvoker))
            {
                request.Headers.Add(SpecialHeaderName, logDelegateInvokerKey.ToString());
            }

        }
    }

    private const string SpecialHeaderName = "__LogDelegateIndex";

    private static long _incrementer = long.MinValue;

    private static readonly ConcurrentDictionary<ulong, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task>> LogDelegateInvokers = 
        new ConcurrentDictionary<ulong, Func<HttpRequestMessage, HttpResponseMessage, Exception, CancellationToken, Task>>();
}

...

此处的关键点是使用自定义委托处理程序创建客户端并对其进行设置,以便委托处理程序在处理请求之前实际调用委托进行日志记录。为支持这一点所做的工作是拥有一个由 64 位键索引的静态线程安全调用字典,在提交请求之前将其呈现为字符串“special”header,然后从header 在发送之前。请注意,header 对 HttpRequestMessage 类型请求的访问是同步的。

我承认这没有完美的代码味道,但它高效且快速。

我仍然很高兴能得到更好的解决方案!

如果您使用的是 .NET Core 3.0 以上的 .NET 版本,您应该能够避免看到已处置的请求。 (apparently there is a bug in older versions that HttpClient auto disposes the request.) 进行了一些简单的更改。使用当前代码,我看到两个可能有问题的问题:

  1. LogDelegateInvoker 可以在空请求上调用,因为如果请求为空,您将抛出 ArgumentNullException 并立即捕获它。
  2. 您正在 catch 块中重新抛出异常,这意味着 LogDelegateInvoker 在调用方 catch 块执行后执行。(查看 this for more info))这将启用在 finally 块被调用之前调用代码来处理请求。它还可能导致 finally 块永远不会执行,具体取决于调用代码。

为了避免这两种情况,您可以:

protected async Task<HttpResponseMessage> SendAsyncInternal(object logContext, HttpRequestMessage request,
    HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    var httpClient = new HttpClient();
    if (request == null)
    {
        throw new ArgumentNullException(nameof(request));
    }

    HttpResponseMessage response = null;
    Exception exception = null;
    try
    {
        response = await httpClient.SendAsync(request, completionOption, cancellationToken);
    }
    catch (Exception e)
    {
        exception = e;
    }
    finally
    {
        await LogDelegateInvoker(logContext, request, response, exception, cancellationToken);
    }

    if (exception != null)
    {
        throw exception;
    }

    return response;
}

这应该可以防止 LogDelegateInvoker 在处置请求中被调用。