博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?
阅读量:4033 次
发布时间:2019-05-24

本文共 6683 字,大约阅读时间需要 22 分钟。

奇怪的问题

最近在公司有个系统需要调用第三方的一个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”的头部。

当时我是震惊的,.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
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
> e = currentActivity.Baggage.GetEnumerator()) { if (e.MoveNext()) { var baggage = new List
(); do { KeyValuePair
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
: IObserver
{ private Action
_next; public MyObserver(Action
next) { _next = next; } public void OnCompleted() { } public void OnError(Exception error) { } public void OnNext(T value) => _next(value); }

订阅HttpHandlerDiagnosticListener:

DiagnosticListener.AllListeners.Subscribe(new MyObserver
(listener => { //判断发布者的名字 if (listener.Name == "HttpHandlerDiagnosticListener") { //获取订阅信息 listener.Subscribe(new MyObserver
>(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 配置环境变量DOTNETSYSTEMNETHTTPENABLEACTIVITYPROPAGATIO=false

  2. 方法3

public class DisableActivityHandler : DelegatingHandler    {        public DisableActivityHandler(HttpMessageHandler innerHandler) : base(innerHandler)        {        }        protected override async Task
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。

转载地址:http://stkdi.baihongyu.com/

你可能感兴趣的文章
[关注大学生]读“贫困大学生的自白”
查看>>
[互联网关注]李开复教大学生回答如何学好编程
查看>>
[关注大学生]李开复给中国计算机系大学生的7点建议
查看>>
[关注大学生]大学毕业生择业:是当"鸡头"还是"凤尾"?
查看>>
[茶余饭后]10大毕业生必听得歌曲
查看>>
gdb调试命令的三种调试方式和简单命令介绍
查看>>
C++程序员的几种境界
查看>>
VC++ MFC SQL ADO数据库访问技术使用的基本步骤及方法
查看>>
VUE-Vue.js之$refs,父组件访问、修改子组件中 的数据
查看>>
Vue-子组件改变父级组件的信息
查看>>
Python自动化之pytest常用插件
查看>>
Python自动化之pytest框架使用详解
查看>>
【正则表达式】以个人的理解帮助大家认识正则表达式
查看>>
性能调优之iostat命令详解
查看>>
性能调优之iftop命令详解
查看>>
非关系型数据库(nosql)介绍
查看>>
移动端自动化测试-Windows-Android-Appium环境搭建
查看>>
Xpath使用方法
查看>>
移动端自动化测试-Mac-IOS-Appium环境搭建
查看>>
Selenium之前世今生
查看>>