在 .NET 裡要解析 URL 參數字串(QueryString,例如: a=1234&b=ABCD),自己拆字串就遜掉了,呼叫 HttpUtility.ParseQueryString()才是王道,這是我很多年前就學到的知識。
最近再有個新發現,ParseQueryString() 所傳回的結果表面上是個 NameValueCollection,但骨子裡則是內部型別 – HttpValueCollection,它有個特異功能,ToString() 被覆寫成可將 Name/Value 再組合還原成 QueryString,所以我們可以用它解析 QueryString,增刪修改參數再 ToString() 轉回 QueryString,十分方便。
不過,試著試著踩到一顆地雷,它的 ToString() 處理中文編碼有問題:
static void TestParseQueryString()
{
var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
var collection = HttpUtility.ParseQueryString(urlQuery);
Console.WriteLine($"Query: {urlQuery}");
Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
Console.WriteLine($"ToString: {collection.ToString()}");
}
實測解析內含 UrlEncode 中文參數再還原,可發現 HttpValueCollection.ToString() 傳回的不是 UTF-8 編碼 UrlEncode,而是過時 %uxxxx 格式 ! (延伸閱讀:【茶包射手日記】勿用 UrlEncodeUnicode 與 escape )
HttpValueCollection 原始碼也證實這點,註解提到是為了向前相容才繼續使用被標為過時(Obsolete)的 UrlEncodeUnicode方法,而程式碼埋了一段偵測 AppSettings.DontUsePercentUUrlEncoding 設定改用 UrlEncode 的邏輯:
// HttpValueCollection used to call UrlEncodeUnicode in its ToString method, so we should continue to
// do so for back-compat. The result of ToString is not used to make a security decision, so this
// code path is "safe".
internal static string UrlEncodeForToString(string input) {
if (AppSettings.DontUsePercentUUrlEncoding) {
// DevDiv #762975: <form action> and other similar URLs are mangled since we use non-standard %uXXXX encoding.
// We need to use standard UTF8 encoding for modern browsers to understand the URLs.
return HttpUtility.UrlEncode(input);
}
else {
#pragma warning disable 618 // [Obsolete]
return HttpUtility.UrlEncodeUnicode(input);
#pragma warning restore 618
}
}
換言之,在 config 加入以下設定即可讓 HttpValueCollection 改用 UrlEncode:
<appSettings><add key="aspnet:DontUsePercentUUrlEncoding" value="true" /></appSettings>
實測成功!
不過,若是在共用函式或公用程式庫使用 HttpValueCollection,要求開發者修改 config 配合太擾民。故還有另一種解法,先用 UrlDecode() 解碼再用 Uri.EscapeUriString() 轉回標準 UTF-8 編碼:
static void TestParseQueryString()
{
var urlQuery = "a=123&b=%E4%B8%AD%E6%96%87";
var collection = HttpUtility.ParseQueryString(urlQuery);
Console.WriteLine($"Query: {urlQuery}");
Console.WriteLine($"ParseQueryString: a={collection["a"]},b={collection["b"]}");
//先UrlDecode解開再使用EscapeUriString置換
var fixedResult = Uri.EscapeUriString(HttpUtility.UrlDecode(collection.ToString()));
//HttpUtility.UrlDecode(collection.ToString()) => a=123&b=中文
//Uri.EscapeUriString("a=123&b=中文") => a=123&b=%E4%B8%AD%E6%96%87
Console.WriteLine($"ToString: {fixedResult}");
}
這樣也能修正問題,報告完畢。