HttpClient 有個 BaseAddress 屬性,若設定妥當,GetAsync() 或 PostAsync() 時可只傳相對路徑,用起來蠻方便的。

但最近我踩到一個雷,當 BaseAddress 只有主機名稱不包含路徑(例如:http://www.host.net),HttpClient 對加不加 / 符號的包容度很高;但如果 BaseAddress 包含路徑(例如:http://www.host.net/api),則 BaseAddress 結尾能不能加 /、GetAsync()/PostAsync() 路徑前方能不能加 / 則有很嚴格的要求,四種組合只有一種可被接受,感覺蠻雷的,寫篇筆記分享。

用以下範例來重現問題,假設我要連到 http://localhost/aspnet/check/test.txt,其內容為字串 "OK"。使用 HttpClient BaseAddress + GetAsync() 下載,我做了兩組測試,BaseAddress 分別為 http://localhost(只有主機名稱) 以及 http://localhost/aspnet(主機名稱加路徑),每組測試會測試 BaseAddress 結尾加或不加 /、GetAsync() 前方加或不加 /,每組有四種組合。

// expect: http://localhost/aspnet/check/test.txt, content == "OK"
// host only
Console.WriteLine($"{"baseAddr",-24}\t{"path",-16}\tresult");
Console.WriteLine("*** host only ***");
await TestHttpClient("http://localhost/", "aspnet/check/test.txt");
await TestHttpClient("http://localhost/", "/aspnet/check/test.txt");
await TestHttpClient("http://localhost", "aspnet/check/test.txt");
await TestHttpClient("http://localhost", "/aspnet/check/test.txt");
// host + path
Console.WriteLine("*** host + path ***");
await TestHttpClient("http://localhost/aspnet/", "check/test.txt");
await TestHttpClient("http://localhost/aspnet/", "/check/test.txt");
await TestHttpClient("http://localhost/aspnet", "check/test.txt");
await TestHttpClient("http://localhost/aspnet", "/check/test.txt");

async Task TestHttpClient(string baseAddr, string path) 
{
    var c = new HttpClient();
    c.BaseAddress = new Uri(baseAddr);
    var r = await c.GetAsync(path);
    var succ = r.StatusCode == System.Net.HttpStatusCode.OK &&
        (await r.Content.ReadAsStringAsync()).Equals("OK");
    Console.WriteLine($"{baseAddr,-24}\t{path,-16}\t{(succ?"PASS":"FAIL")}");
}

測試結果如下:(註:第二組測試標題有誤,應為 host + path)

由實測結果可知,當 BaseAddress 只有主機名稱時,BaseAddress 及 GetAsync() 加不加 / 無所謂,怎麼都會成功;但如果 BaseAddress 包含路徑,只有「BaseAddress 結尾要加 /、GetAsync()/PostAsync() 前方不加 /」的組合才會成功,如要使用 HttpClient BaseAddress 需注意!

參考:Why is HttpClient BaseAddress not working? by Stackoverflow

為什麼會有這種行為?BaseAddress 文件 有提到 BaseAddress 依循 RFC 3986 規範,最右一個 / 之後的部分會被捨棄。而在這篇討論,微軟 RD 進一步做了說明。

依據 RFC3986 規範的 URI 合併規則,當 BaseAddress 含路徑但結尾未加 / (如 http://www.host.net/api),結尾的 api 會被視為檔案名稱捨去,變成 http://www.host.net/,不管 GetAsync() 傳入 URI 為何,結果都缺少 api。若 BaseAddress 為 http://www.host.net/api/,api 被視為資料夾名稱保留,GetAsync() 若前方有 /,會組出 http://www.host.net/api//check/test.txt 而無效,故只有 TestHttpClient("http://localhost/aspnet/", "check/test.txt")‵ 會成功。

用 Uri 做實驗 var u = new Uri(new Uri(baseAddr), path); 也會得到相同的結果:

微軟 RD 解釋,這個規範已存在快 20 年,無論你多不喜歡都得遵守,更動 Uri 行為會導致依賴它的既有程式出錯,並與大部分人期望的結果不同。

心得:HttpClient 看似詭異的行為其實是依循 URI 規範的結果,弄懂原理後有豁然開朗的感覺,原本覺得怪,現在再看就合理多了。:D

Tips of how to set HttpClient BaseAddress correctly.


Comments

# by Huang

老人如我,對於路徑都用古早的DOS檔案與目錄觀念來看待。因此看到URL的目錄aspnet、check不管和誰在一起,前後沒加/來表示目錄才覺得不習慣,就剛好避雷了,哈哈(要是加了變//是另一回事…XD)

# by 小黑

實用

Post a comment