奇怪的問題
最近在公司有個系統需要呼叫第三方的一個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<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 one rror(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 =https://www.cnblogs.com/kklldog/p/>
{
System.Console.WriteLine($"監聽名稱:{listenerData.Key}");
dynamic data = https://www.cnblogs.com/kklldog/p/listenerData.Value;
}));
}
}));
當我們訂閱HttpHandlerDiagnosticListener的時候HttpClient發送的請求就會帶上這個頭部,這個設計的真的比較變態,因為DiagnosticListener.AllListeners是靜態的,所以它的影響是全域的,也就是說我這里訂閱了一個監聽,會導致整個程式中所有的HttpClient都開始帶上這個頭部,
這也解釋了為何我們的程式運行一段時間之后才帶上Request-Id的頭部,因為我們程式中其它模塊,或者參考的三方庫的在達到某種狀態的時候會開始訂閱HttpHandlerDiagnosticListener這個監聽,導致我請求webservice的代碼也帶上了這個頭部,
解決問題
問題的原因也找到了,本地也復現了,現在我們要開始真正的解決問題了,經過google跟查看原始碼,要讓HttpClient不發送這個Request-Id頭部有幾種辦法,
- 方法1
設定System.Net.Http.EnableActivityPropagation開關為false
string switchName = "System.Net.Http.EnableActivityPropagation";
AppContext.SetSwitch(switchName, false);
-
方法2
配置環境變數DOTNET_SYSTEM_NET_HTTP_ENABLEACTIVITYPROPAGATIO=false -
方法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,
關注我的公眾號一起玩轉技術

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/207636.html
標籤:.NET Core
