在 .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}");
}

這樣也能修正問題,報告完畢。


Comments

Be the first to post a comment

Post a comment


73 - 22 =