.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?
奇怪的問題
最近在公司有個系統(tǒng)需要調(diào)用第三方的一個webservice。本來調(diào)用一個下很簡單的事情,使用HttpClient構(gòu)造一個SOAP請求發(fā)送出去拿到XML解析就是了。可奇怪的是我們的請求在運行一段時間后就會被服務(wù)器504給拒絕掉了。導(dǎo)致系統(tǒng)無法使用,用戶叫苦連天。古怪就古怪在這個問題不是每次都會出現(xiàn),是隔三差五的查詢,每次修改完代碼發(fā)布上去以為好了, 過了兩天又不行了,簡直讓人奔潰。
Postman測試
在反復(fù)調(diào)試代碼無果的情況下,我懷疑是對方服務(wù)器的問題。于是拿出Postman往對方服務(wù)器發(fā)送請求測試。postman測試一測就測出問題了,不管發(fā)送什么,服務(wù)器全部給出了504的響應(yīng)。因為在瀏覽器里訪問webservice的首頁是可以的,但是為什么在postman上面就不行了呢?于是我開始反復(fù)檢查postman的請求有何不同,到這里感覺離發(fā)現(xiàn)問題不遠(yuǎn)了。在反復(fù)查看下我開始懷疑是postman的一個頭部的問題:
Postman-Token: 4d407574-636b-9343-8216-7f2845cbeef1postman每次發(fā)送請求的時候都會帶上一個叫做postman-token的頭部。于是我把這個頭部給禁用了再試一次,果斷成功了。在反復(fù)測試下終于明白了,對方服務(wù)器應(yīng)該有防護,只要http請求里帶有自定義的頭部就會直接給出504的響應(yīng),直接拒絕請求。至此服務(wù)器拒絕請求的原因終于明了了。
fiddler監(jiān)控
但是,我們的代碼發(fā)送請求的時候并沒有帶上任何自定義的頭部啊。莫非.NET Core會在發(fā)送請求的時候帶上什么頭部嗎?于是在服務(wù)器上安裝fiddler,把請求通過fiddler代理轉(zhuǎn)發(fā)出去,然后監(jiān)控http請求的頭部。當(dāng)系統(tǒng)再次出現(xiàn)問題的時候 果斷上去查看fiddler。一看果然發(fā)現(xiàn)了問題,所有被拒絕的請求都帶上了一個叫“Request-Id”的頭部。
當(dāng)時我是震驚的,.NetCore居然會自說自話給我加上一個頭部?如果不是親身發(fā)現(xiàn),打死我也不會相信的。或許你看到這里也還是不相信,心里在想一定是我搞錯了吧。
Request-Id頭部到底哪里來的?
這個問題真是百思不得其解,于是開始請教google。很快在.net core runtime的github上的issues發(fā)現(xiàn)一個同樣的問題:
HttpClient automatically adds Request-Id HTTP header
提問的人說使用HttpClient發(fā)送請求的時候莫名其妙加上了一個Request-Id,跟我情況一毛一樣。
于是乎有人開始討論。有人說HttpClient不可能自己加上Request-Id這個頭部的,下面的老哥直接打臉,說:事實上會的,還給出了源碼的位置。笑哭!后來還有開發(fā)者回復(fù)這個功能是內(nèi)置的,是為了分布式追蹤。
既然源碼都給出來了,直接從上面老哥給出的源碼位置開始追源碼。下面大概說一下源碼:
HttpClient默認(rèn)構(gòu)造函數(shù):
繼續(xù)看里面的HttpClientHandler:
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken cancellationToken){return DiagnosticsHandler.IsEnabled() ?_diagnosticsHandler.SendAsync(request, cancellationToken) :_socketsHttpHandler.SendAsync(request, cancellationToken);}HttpClientHandler發(fā)送請求的時候會判斷是否使用diagnosticsHandler來發(fā)送請求。繼續(xù)看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 itemsusing (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}終于找到關(guān)鍵的位置了有個叫InjectHeaders的方法里面有這么一句 request.Headers.TryAddWithoutValidation(DiagnosticsHandlerLoggingStrings.RequestIdHeaderName, currentActivity.Id);其中DiagnosticsHandlerLoggingStrings.RequestIdHeaderName是個常量,它的值就是"Request-Id"。
到這里是誰帶上的Request-Id頭部的問題終于石錘了。
復(fù)現(xiàn)問題
原因找到了,于是開始測試解決辦法。解決問題的第一步是先復(fù)現(xiàn)問題。正常情況下你使用HttpClient發(fā)送請求時不會帶上這個頭部的。要讓本地發(fā)送的請求也帶上這個頭部也不是件容易的事。經(jīng)過查看源代碼發(fā)現(xiàn)其實是跟.net core的Diagnostics機制有關(guān)。由于源碼邏輯比較復(fù)雜,直接給出會帶上頭部的代碼:
首先定義一個Observer:
訂閱HttpHandlerDiagnosticListener:
DiagnosticListener.AllListeners.Subscribe(new MyObserver<DiagnosticListener>(listener =>{//判斷發(fā)布者的名字if (listener.Name == "HttpHandlerDiagnosticListener"){//獲取訂閱信息listener.Subscribe(new MyObserver<KeyValuePair<string, object>>(listenerData =>{System.Console.WriteLine($"監(jiān)聽名稱:{listenerData.Key}");dynamic data = listenerData.Value;}));}}));當(dāng)我們訂閱HttpHandlerDiagnosticListener的時候HttpClient發(fā)送的請求就會帶上這個頭部。這個設(shè)計的真的比較變態(tài),因為DiagnosticListener.AllListeners是靜態(tài)的,所以它的影響是全局的。也就是說我這里訂閱了一個監(jiān)聽,會導(dǎo)致整個程序中所有的HttpClient都開始帶上這個頭部。
這也解釋了為何我們的程序運行一段時間之后才帶上Request-Id的頭部。因為我們程序中其它模塊,或者引用的三方庫的在達到某種狀態(tài)的時候會開始訂閱HttpHandlerDiagnosticListener這個監(jiān)聽,導(dǎo)致我請求webservice的代碼也帶上了這個頭部。
解決問題
問題的原因也找到了,本地也復(fù)現(xiàn)了,現(xiàn)在我們要開始真正的解決問題了。經(jīng)過google跟查看源碼,要讓HttpClient不發(fā)送這個Request-Id頭部有幾種辦法。
方法1
設(shè)置System.Net.Http.EnableActivityPropagation開關(guān)為false
string switchName = "System.Net.Http.EnableActivityPropagation"; AppContext.SetSwitch(switchName, false);方法2 配置環(huán)境變量DOTNETSYSTEMNETHTTPENABLEACTIVITYPROPAGATIO=false
方法3
該方法定義一個DisableActivityHandler再構(gòu)造HttpClient,在每次發(fā)送請求的時候都把Activity.Current置空。
總結(jié)
最近被這個Request-Id折騰了很久。這里忍不住要吐槽下,這個內(nèi)置的功能真的好嗎,強力插入自定義頭部,有考慮過防火墻的感受嗎?或者是不是可以讓開發(fā)者主動選擇是否計入Diagnostic統(tǒng)計,而不是某一處開始訂閱就全部請求都添加頭部,畢竟我們無法控制第三方的庫是否有什么騷操作。如果要關(guān)閉這個Diagnostic是不是可以在HttpClient實例上直接給出一個明確的開關(guān)讓開發(fā)者關(guān)閉它,而不是需要配置什么環(huán)境變量。
ps:如果是使用HttpWebRequest類發(fā)送請求同樣有這個問題,因為HttpWebRequest發(fā)送請求的時候就是用的HttpClient。
總結(jié)
以上是生活随笔為你收集整理的.NetCore HttpClient发送请求的时候为什么自动带上了一个RequestId头部?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET 5 和 C#9 /F#5 一起
- 下一篇: ASP.NETCore小技巧:使用测试用