zoukankan      html  css  js  c++  java
  • .NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?

    奇怪的问题

    最近在公司有个系统需要调用第三方的一个webservice。本来调用一个下很简单的事情,使用HttpClient构造一个SOAP请求发送出去拿到XML解析就是了。
    可奇怪的是我们的请求在运行一段时间后就会被服务器504给拒绝掉了。导致系统无法使用,用户叫苦连天。
    古怪就古怪在这个问题不是每次都会出现,是隔三差五的查询,每次修改完代码发布上去以为好了,
    过了两天又不行了,简直让人奔溃。

    Postman测试

    在反复调试代码无果的情况下,我怀疑是对方服务器的问题。于是拿出Postman往对方服务器发送请求测试。
    postman测试一测就测出问题了,不管发送什么,服务器全部给出了504的响应。因为在浏览器里访问webservice的首页是可以的,但是为什么在postman上面就不行了呢?
    于是我开始反复检查postman的请求有何不同,到这里感觉离发现问题不远了。在反复查看下我开始怀疑是postman的一个头部的问题:

    Postman-Token: 4d407574-636b-9343-8216-7f2845cbeef1
    

    postman每次发送请求的时候都会带上一个叫做postman-token的头部。于是我把这个头部给禁用了再试一次,果断成功了。
    在反复测试下终于明白了,对方服务器应该有防护,只要http请求里带有自定义的头部就会直接给出504的响应,直接拒绝请求。
    至此服务器拒绝请求的原因终于明了了。

    fiddler监控

    但是,我们的代码发送请求的时候并没有带上任何自定义的头部啊。莫非.NET Core会在发送请求的时候带上什么头部吗?
    于是在服务器上安装fiddler,把请求通过fiddler代理转发出去,然后监控http请求的头部。当系统再次出现问题的时候
    果断上去查看fiddler。一看果然发现了问题,所有被拒绝的请求都带上了一个叫“Request-Id”的头部。
    BTgP1A.png
    当时我是震惊的,.NetCore居然会自说自话给我加上一个头部?
    如果不是亲身发现,打死我也不会相信的。或许你看到这里也还是不相信,心里在想一定是我搞错了吧。

    Request-Id头部到底哪里来的?

    这个问题真是百思不得其解,于是开始请教google。很快在.net core runtime的github上的issues发现一个同样的问题:
    HttpClient automatically adds Request-Id HTTP header
    提问的人说使用HttpClient发送请求的时候莫名其妙加上了一个Request-Id,跟我情况一毛一样。
    于是乎有人开始讨论。有人说HttpClient不可能自己加上Request-Id这个头部的,下面的老哥直接打脸,说:事实上会的,还给出了源码的位置。笑哭!后来还有开发者回复这个功能是内置的,是为了分布式追踪。
    既然源码都给出来了,直接从上面老哥给出的源码位置开始追源码。下面大概说一下源码:
    HttpClient默认构造函数:

      public HttpClient()
                : this(new HttpClientHandler())
            {
            }
    

    继续看里面的HttpClientHandler:

       protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                CancellationToken cancellationToken)
            {
                return DiagnosticsHandler.IsEnabled() ?
                    _diagnosticsHandler.SendAsync(request, cancellationToken) :
                    _socketsHttpHandler.SendAsync(request, cancellationToken);
            }
    

    HttpClientHandler发送请求的时候会判断是否使用diagnosticsHandler来发送请求。继续看diagnosticsHandler的代码:

      private static void InjectHeaders(Activity currentActivity, HttpRequestMessage request)
            {
                if (currentActivity.IdFormat == ActivityIdFormat.W3C)
                {
                    if (!request.Headers.Contains(DiagnosticsHandlerLoggingStrings.TraceParentHeaderName))
                    {
                        request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.TraceParentHeaderName, currentActivity.Id);
                        if (currentActivity.TraceStateString != null)
                        {
                            request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.TraceStateHeaderName, currentActivity.TraceStateString);
                        }
                    }
                }
                else
                {
                    if (!request.Headers.Contains(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName))
                    {
                        request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName, currentActivity.Id);
                    }
                }
    
                // we expect baggage to be empty or contain a few items
                using (IEnumerator<KeyValuePair<string, string?>> e = currentActivity.Baggage.GetEnumerator())
                {
                    if (e.MoveNext())
                    {
                        var baggage = new List<string>();
                        do
                        {
                            KeyValuePair<string, string?> item = e.Current;
                            baggage.Add(new NameValueHeaderValue(WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)).ToString());
                        }
                        while (e.MoveNext());
                        request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.CorrelationContextHeaderName, baggage);
                    }
                }
            }
    
            private static readonly DiagnosticListener s_diagnosticListener =
                new DiagnosticListener(DiagnosticsHandlerLoggingStrings.DiagnosticListenerName);
    
            #endregion
        }
    

    终于找到关键的位置了有个叫InjectHeaders的方法里面有这么一句 request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName, currentActivity.Id);其中DiagnosticsHandlerLoggingStrings.RequestIdHeaderName是个常量,它的值就是"Request-Id"。
    到这里是谁带上的Request-Id头部的问题终于石锤了。

    复现问题

    原因找到了,于是开始测试解决办法。解决问题的第一步是先复现问题。正常情况下你使用HttpClient发送请求时不会带上这个头部的。要让本地发送的请求也带上这个头部也不是件容易的事。经过查看源代码发现其实是跟.net core的Diagnostics机制有关。由于源码逻辑比较复杂,直接给出会带上头部的代码:
    首先定义一个Observer:

        public class MyObserver<T> : IObserver<T>
        {
            private Action<T> _next;
            public MyObserver(Action<T> next)
            {
                _next = next;
            }
    
            public void OnCompleted()
            {
            }
    
            public void OnError(Exception error)
            {
            }
    
            public void OnNext(T value) => _next(value);
        }
    
    

    订阅HttpHandlerDiagnosticListener:

        DiagnosticListener.AllListeners.Subscribe(new MyObserver<DiagnosticListener>(listener =>
                {
                    //判断发布者的名字
                    if (listener.Name == "HttpHandlerDiagnosticListener")
                    {
                        //获取订阅信息
                        listener.Subscribe(new MyObserver<KeyValuePair<string, object>>(listenerData =>
                        {
                            System.Console.WriteLine($"监听名称:{listenerData.Key}");
                            dynamic data = listenerData.Value;
                        
                        }));
    
                    }
                }));
    

    当我们订阅HttpHandlerDiagnosticListener的时候HttpClient发送的请求就会带上这个头部。这个设计的真的比较变态,因为DiagnosticListener.AllListeners是静态的,所以它的影响是全局的。也就是说我这里订阅了一个监听,会导致整个程序中所有的HttpClient都开始带上这个头部。
    这也解释了为何我们的程序运行一段时间之后才带上Request-Id的头部。因为我们程序中其它模块,或者引用的三方库的在达到某种状态的时候会开始订阅HttpHandlerDiagnosticListener这个监听,导致我请求webservice的代码也带上了这个头部。

    解决问题

    问题的原因也找到了,本地也复现了,现在我们要开始真正的解决问题了。经过google跟查看源码,要让HttpClient不发送这个Request-Id头部有几种办法。

    1. 方法1

    设置System.Net.Http.EnableActivityPropagation开关为false

    string switchName = "System.Net.Http.EnableActivityPropagation";
    AppContext.SetSwitch(switchName, false);
    
    1. 方法2
      配置环境变量DOTNET_SYSTEM_NET_HTTP_ENABLEACTIVITYPROPAGATIO=false

    2. 方法3

        public class DisableActivityHandler : DelegatingHandler
        {
            public DisableActivityHandler(HttpMessageHandler innerHandler) : base(innerHandler)
            {
    
            }
    
            protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            {
                Activity.Current = null;
    
                return await base.SendAsync(request, cancellationToken);
            }
        }
    
        var httpClient = new HttpClient(new DisableActivityHandler(new HttpClientHandler()));
    

    该方法定义一个DisableActivityHandler再构造HttpClient,在每次发送请求的时候都把Activity.Current置空。

    总结

    最近被这个Request-Id折腾了很久。这里忍不住要吐槽下,这个内置的功能真的好吗,强力插入自定义头部,有考虑过防火墙的感受吗?或者是不是可以让开发者主动选择是否计入Diagnostic统计,而不是某一处开始订阅就全部请求都添加头部,毕竟我们无法控制第三方的库是否有什么骚操作。如果要关闭这个Diagnostic是不是可以在HttpClient实例上直接给出一个明确的开关让开发者关闭它,而不是需要配置什么环境变量。

    ps:如果是使用HttpWebRequest类发送请求同样有这个问题,因为HttpWebRequest发送请求的时候就是用的HttpClient。

    关注我的公众号一起玩转技术

  • 相关阅读:
    Win10 UWP Tile Generator
    Win10 BackgroundTask
    UWP Tiles
    UWP Ad
    Win10 build package error collections
    Win10 八步打通 Nuget 发布打包
    Win10 UI入门 pivot multiable DataTemplate
    Win10 UI入门 导航滑动条 求UWP工作
    UWP Control Toolkit Collections 求UWP工作
    Win10 UI入门 SliderRectangle
  • 原文地址:https://www.cnblogs.com/kklldog/p/why-httpclient-auto-add-header-request-id.html
Copyright © 2011-2022 走看看