今天踩到一個坑,發現 WebClient 有個我沒注意過的行為。

試著用 WebClient 呼叫 SharePoint 的 REST API,怎麼試都不成功。因為是第一次寫,優先想到的是我漏了某個必要參數或忽略關鍵步驟,而用錯誤訊息爬文,查到的案例幾乎都是未設定 Content-Type 所致,而我很確定有設定 WebClient.Headers.Add("Content-Type", "application/json;odata=verbose") 及 WebClient.Headers.Add("Accept", "application/json;odata=verbose"),而且是兩次呼叫第一次成功,第二次才噴出 <?xml version="1.0" encoding="utf-8"?><m:error xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"><m:code>-1,System.Collections.Generic.KeyNotFoundException</m:code><m:message xml:lang="en-US">The given key was not present in the dictionary.</m:message></m:error>

鬼打牆好一陣子,直到我改用 PowerShell Invoke-WebRequest,發現一模一樣的 POST 請求內容用 PowerShell 發送可以成功,趕緊用 Fiddler 側錄比較,這才鎖定問題。

試著用以下 ASPX 重現問題,ASPX 預設由 Query String 取參數,當 Content-Type 為 application/json 時,則改由 HTTP Body 讀取 JSON 解析取參數,同時有個自訂 Header X-ApiKey:

<%@Page Language="C#"%>
<script runat="server">
    void Page_Load(object sender, EventArgs e) 
    {
        var cookie = Request.Headers["X-ApiKey"];
        var x = Request.QueryString["x"];
        var y = Request.QueryString["y"];
        if (Request.ContentType.StartsWith("application/json")) 
        {
            using (var sr = new System.IO.StreamReader(Request.InputStream)) {
                var d = new System.Web.Script.Serialization.JavaScriptSerializer()
                    .Deserialize<Dictionary<string, string>>(sr.ReadToEnd());
                x = d["x"];
                y = d["y"];
            }
        }
        Response.Write(string.Format("X-ApiKey = {0}, X = {1}, Y = {2}",
            cookie, x, y));
    }
</script>

客戶端如下:(實際案例發生在 .NET Framework 4.x,但用 .NET 6 測試也是相同結果)

var wc = new WebClient();
wc.UseDefaultCredentials = true;
wc.Headers.Add("Accept", "*/*");
wc.Headers.Add("Content-Type", "application/json");
wc.Headers.Add("X-ApiKey", "Secret");
var url = "http://localhost/aspnet/postjson/";
var res = wc.UploadString(url, @"{ ""x"": ""1"", ""y"": ""2"" }");
Console.WriteLine(res);
res = wc.UploadString(url, @"{ ""x"": ""3"", ""y"": ""4"" }");
Console.WriteLine(res);

測試第一次 deafult.aspx 有接到 JSON 傳入 x, y 及自訂 X-ApiKey Header,但第二次只有 X-ApiKey,沒有 x, y:

使用 Fiddler 比對兩次送的 HTTP Request 內容,可以發現第二次少了 Accept 及 Content-Type Header:

這可以解釋為什麼我第二次送出 SharePoint REST API 會得缺少 Content-Type 導致的錯誤訊息(不過,KeyNotFoundException 頗具誤導性),且傳回結果是 XML 而非 JSON (因為沒傳 Accept 指定接收 JSON)。

至於為什麼第二次送出時 Content-Type 跟 Accept Header 會消失,但 X-ApiKey 還在?微軟文件是這麼說的:

You should not assume that the header values will remain unchanged, because Web servers and caches may change or add headers to a Web request.
你不應該假設 Header 值會一直保持不變,網站伺服器及 Cache 可能改變或增加 Web Request 的 Header 項目。

結案。

WebClient header values may change after sending request, remeber to reset them if you want to reuse it.


Comments

# by JerryH

找到一篇文章,是不要用WebClient.UploadData method 就不會有問題? https://stackoverflow.com/questions/3865193/how-to-change-header-in-webclient/8472039

# by 小海

這個不錯,寫黑大

Post a comment